You are on page 1of 27

CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Tài liệu

Đặc tả kiến trúc dự án

MOBILE APP DỊCH VỤ CÔNG


BẮC GIANG

Version 1.0

Prepared by TranVietDuc

Năm 2022

Trang 1/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

LỊCH SỬ THAY ĐỔI TÀI LIỆU


Ngày thay
Phiên bản Mô tả Tác giả/Nhóm tác giả
đổi

Trang 2/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

MỤC LỤC
1. GIỚI THIỆU CHUNG
1.1 Mục đích
1.2 Phạm vi
1.3 Các định nghĩa, thuật ngữ, từ viết tắt
1.4 Tài liệu tham khảo

2. TỔNG QUAN KIẾN TRÚC DỰ ÁN


2.1 Folder lib/src/core
2.2 Folder lib/src/services
2.3 Folder lib/src/navigations
2.4 Folder lib/src/features

3. CÁC CÔNG NGHỆ HIỆN CÓ TRONG DỰ ÁN


3.1 Kiến trúc Clean Architecture TDD
3.2 Các quy tắc khai báo đối tượng/ class
3.3 Các parttens được triển khai đễ hỗ trợ duy trì, mở rộng dự án

1. HƯỚNG DẪN CLONE VÀ BUILD APP LẦN ĐẦU


1.1 Chuẩn bị
1.2 Build app lần đầu
GIỚI THIỆU CHUNG
1.1 Mục đích

Tài liệu Đặc tả kiến trúc dự án mô tả một cách đầy đủ, toàn diện các kiến trúc, công nghệ sử
dụng trong Mobile App Dịch vụ công Bắc Giang – đó là các cách phân tầng source code, chức
năng. Tài liệu này sẽ hỗ trợ các lập trình viên trong quá trình phát triển, duy trì, mở rộng, bảo
hành bảo trì dự án và giúp đội ngũ phát triển có sự đồng bộ.
Tài liệu bao gồm kiến trúc chung, cách triển khai và vận hành kiến trúc Clean-Architecture,
các tính năng cốt lõi, bao gồm ví dụ và các mô tả chi tiết liên quan về phiên bản, nguồn tham
khảo.
1.2 Phạm vi

● Kiến trúc dự án

● Các tính năng cốt lõi và cách triển khai Protocol

● Nguồn tham khảo

● Đối tượng sử dụng:


Giúp đội ngũ phát triển, đặc biệt là đội ngũ Flutter – Mobile App dễ dàng hơn trong việc triển
khai, mở rộng và duy trì dự án, dựa vào đây để xây dựng hệ thống chính xác.
1.3 Các định nghĩa, thuật ngữ, từ viết tắt

STT Thuật ngữ, từ viêt tắt Giải thich Ghi chú


1 AP Administrative
Procedure - Thủ tục
hành chính

Trang 3/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

1.4 Tài liệu tham khảo

STT Tên tài liệu Ghi chú


1 Flutter TDD – Clean Architecture https://resocoder.com/flutter-
clean-architecture-tdd/
2 Bloc State management - Example https://bloclibrary.dev/
3 Flutter Package’s Docs https://pub.dev/

Trang 4/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

2. TỔNG QUAN KIẾN TRÚC DỰ ÁN


Dịch vụ công bắc giang được viết trên ngôn ngữ Dart, chạy trên Flutter framework, s ử
dụng để phát triển Mobile app đa nền tảng bao gồm IOS và Android.
Dự án được xây dựng trên kiến trúc Feature-first, được hiểu là phân tách và đóng gói
các luồng tính năng như các sub-module riêng biệt, bao gồm cả UI và Business Logic.
Cụ thể như sau:
2.1 Folder lib/src/core

Hình 1. Tổng quan cách bố trí các folder trong source code

Bao gồm các design system, controller chính của toàn App, các common UI
widgets, các validator, common responses, và design chung (màu sắc, cỡ chữ, căn
lề,…) của toàn App.

Trang 5/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

2.2 Folder lib/src/helpers và lib/src/services

Hình 2. Cách bố trí folder lib/src/helpers và lib/src/services

Helpers và Serivices (nằm trong lib/src/ ) bao gồm các đoạn mã tự viết nhằm
tương thích với các logic, các yêu cầu từ BA - BE.

2.3 Folder lib/src/navigations

Hình 3. Cách bố trí folder lib/src/navigations

Đây là nơi chứa toàn bộ những gì liên quan đến routes - navigation của dự án,
bao gồm observer, routes và animation/transition.

Trang 6/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

2.4 Folder lib/src/features

Hình 4. Cách bố trí folder lib/src/navigations

Folder lib/srcc/features là nơi chứa các đoạn mã chính để phát triển tính năng
của dự án. Trong đó, mỗi tính được phân tách thành ba phần tuân thủ theo kiến trúc
Domain - Driven - Design (tuy nhiên có các trường hợp ngoại lệ khi tính năng chưa
đủ phức tạp, để tiết kiệm thời gian phát triển, đội dê đã gộp chung hai Domain Layer
vào Data Layer, trong đó, mỗi tính năng đều dược thiết kế tuân thủ kiến trúc MVVM,
phân tách tường minh các thành phần Model (domain/data)- View Model - View
(presentations). Cách xây dựng chi tiết được dựa theo và tham khảo tại:
https://resocoder.com/flutter-clean-architecture-tdd/

Trang 7/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

3. CÁC CÔNG NGHỆ HIỆN CÓ TRONG DỰ ÁN


3.1 Kiến trúc Clean Architecture TDD

● Tổng quan: TDD, hay Test - Driven - Dev, xây dựng dựa trên Clean Architecture, có
thể hình dung bằng sơ đồ sau:

Hình 5. Tổng quan về Clean Architecture – Flutter TDD

● Trong đó, đối với Dart OOP, các khái niệm tương đồng bao gồm:

● ENTITIES => MODELS

● USER CASES => SERVICES

● CONTROLLERS - GATEWAYS - PRESENTERS => BLOC (state


management áp dụng trong dự án)

● TDD tuyệt đối tuân thủ luồng dữ liệu MỘT CHIỀU từ data => UI theo chiều mũi tên,
điểm cuối mũi tên sẽ chịu ảnh bưởng từ điểm ở đầu mũi tên, tức là User Case sẽ chỉ
chịu ảnh hưởng bởi Entities, còn Entities sẽ không chịu ảnh hưởng bởi bất kỳ thứ gì.

Trang 8/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Hình 6. Mô tả chi tiết luồng Call Follow và tính phụ thuộc giữa các thành phần trong dự án

Trang 9/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

** Giải thích ý nghĩa và cách tổ chức Code

● Mọi tính năng, đơn cử như “Tra cứu thủ tục hành chính” dưới đây, sẽ được chia làm
ba tầng chính, gồm Presentation, Domain và Data, để tiết kiệm thời gian, dev sử dụng
EXT này để tự động generate các Folder:
o https://marketplace.visualstudio.com/items?itemName=KiritchoukC.flutter-
clean-architecture

Hình 7. Phân bố các folder trong một tính năng

Áp dụng trong dự án
Mỗi luồng tính năng sẽ có một bloc duy nhất để điều khiển và quản lý State của
luồng tính năng ấy, đơn cử như tính năng “Lấy thông tin thủ tục hành chính” sau đây.
Trong đó, Bloc sẽ chịu trách nhiệm quản lý mọi State bao gồm các State UI và các
State logic, cách triển khai Bloc sẽ được mô tả kỹ trong phần sau.

Trang 10/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Mô tả chi tiết:

● Tầng Presentation

Hình 8. Phân bố các folder trong thư mục Presentation

Presentation ta có các Pages (trong đó mỗi page là một Screen, nơi chứa bộ khung
UI và TOÀN BỘ CÁC ĐIỂM GỌI BUSINESS LOGIC có trong page đó, nghĩa là khi
muốn tìm các điểm gọi Event, xử lý dữ liệu nguồn, ta sẽ tìm ở trong Page).

Trang 11/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Hình 9. Phân bố các folder trong thư mục Pages

Trang 12/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Thứ hai là Widgets, nơi chứa các widget được tái sử dụng, tại đây đơn thuần là nhận
models, triển khai UI trên các model đó, MỌI FUNCTION / METHOD ĐƯỢC GỌI
TRÊN WIDGET PHẢI ĐƯỢC TRUYỀN VÀO TỪ PAGE, như trong hình, hàm “chuyển
trang” và “tương tác chạm” phải được khai báo ở page chứa file View và truyền vào
theo params.

Hình 10. Phân bố các folder trong thư mục Pages

● Tầng Domain
Domain là nơi chứa các Abstract Class, Các models được lấy trực tiếp từ
DB/Server/API, trong Domain chia làm 2 phần chính, đó là repository - Nơi chứa
Business logic (Cả phần trừu tượng và phần triển khai của những Call-API-Functions),
vì Flutter hay Dart không có khái niệm Interface hay Protocols, nên ta buộc ph ải chia
các BUSINESS logic thành hai phần: trừu tượng và triển khai, nhằm triển khai testing
và Coding mà không ảnh hưởng đến Code của nhau, cụ thể phương thức triển khai
testing, tôi sẽ đề cập ở phần sau.
Quay trở lại với domain, trước tiên ta đi vào các Abstract Class.

abstract class AdministrativeProceduresRepo {


Future<dynamic> getSuggestionSectors();
Future<dynamic> getSuggestionNames();
Future<dynamic> getAdministrativeProcedureDetails(
AdministrativeProcedureSearchForm form);
Future<dynamic> getPopularAdministrativeProcedures();
Future<dynamic> getAdministrativeProceduresByParrams(
AdministrativeProcedureSearchForm? form,
String? textSearch,
int? pageNumber);
Future<dynamic> postInfoAdministrativeProcedure(String id);

Trang 13/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Đây là nơi ta định nghĩa các function xuất hiện trong luồng tính năng (lưu ý: do đội
ngũ BackEnd của dự án đã dựng nên hệ thống backend khá chất lượng, nên việc
triển khai các Abstract Funcition này có thể trả về dynamic, vì các kiểu dữ liệu trả về
đã được đội BE ép kiểu thành các kiểu cố định, chắc chắn không có exceptions rồi),
các class này sẽ dùng làm phôi kế thừa cho phần các class triển khai và
MockingClass trong thư mục test.
Tiếp Theo là class triển khai của các Abstract Class đó.

class AdministrativeProceduresRepoImpl implements AdministrativeProceduresRepo {


final Dio dio = Dio()
// ..interceptors.add(
// PrettyDioLogger(),
// )
..interceptors.add(
BGEServicesInterceptorNoLoading(),
);

@override
Future getAdministrativeProcedureDetails(
AdministrativeProcedureSearchForm form) async {}

@override
Future getSuggestionSectors() async {
try {
final sectorsResponse =
await dio.get(GET_ADMINISTRATIVE_PROCEDURES_SECTORS_URL);
if (sectorsResponse.data["status"] == 1 &&
sectorsResponse.data["data"] != null &&
sectorsResponse.data["data"].length > 0) {
final AdministrativeProcedureSearchResponse
administrativeProcedureSearchResponse =
AdministrativeProcedureSearchResponse.fromMap(sectorsResponse.data);
final List<AdministrativeProcedureData> data =
administrativeProcedureSearchResponse.data ?? [];
final List<String> sectorStrings = data.map((e) => e.linhVuc).toList();
return sectorStrings;
}
return [];
} catch (e) {
return FailureResponse();
}
}

Tại đây là nơi ta viết hàm triển khai, lấy dữ liệu từ API, và Render ra object, vì tuân th ủ
kiến trúc MVVM cho mỗi tính năng nên mọi object đều phải được decode tại đây.
Cách lấy data và decode object sẽ được tôi mô tả tại phần sau.

● Tầng Data

Trang 14/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Hình 11. Phân bố các folder trong thư mục Data

Trang 15/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Trong data, ta sẽ phân vùng các data_source, nếu các data đơn giản (từ các kiểu dữ
liệu khởi nguyên), còn nếu data cần lưu là các object phức tạp thì ta phải triển khai cụ
thể data_source đó trong một file tường minh khác.

- Local_data_source là nơi ta gọi các hàm lấy dữ liệu từ local Storage, trong dự
án, đội ngũ phát triển sử dụng 3 thư viện để lưu data xuống local Đó là Hive,
Shared_Preference và Secure_Storage, cách triển khai và sử dụng các thư viện này
đã được mô tả kỹ càng tại README của thư viện đó, Chi tiết về phiên bản của các
thư viện này được liệt kê tại file Stack công nghệ của dự án.
- Remote_data_source là nơi ta triển khai các nguồn dữ liệu từ xa, sử dụng
trong trường hợp data được lấy từ nhiều nguồn và quy nạp thành một object lớn, hoặc
phân nhỏ gói tin để tối ưu quá trình truyền tải dữ liệu (hoặc do yêu cầu bảo mật/cách
triển khai của BE), tạm thời chưa Apply điều này trong dự án, mà được triển khai trực
tiếp trong file Repositories thuộc lớp Domain, tài liệu mô tả phần này có chi tiết trên
https://resocoder.com/2019/10/03/flutter-tdd-clean-architecture-course-9-remote-
data-source/
- Các data_soucre phức tạp: Nơi ta triển khai việc lưu các data phức tạp,
dạng object hoặc hơn thế, điều này vẫn đang trong giai đoạn phát triển và chỉnh sửa,
nên các tài liệu mô tả sẽ được cập nhật sau.

Trên đây là toàn bộ mô tả về KIẾN TRÚC DỰ ÁN, phần sau đây tôi sẽ giới
thiệu đến CÁC PHƯƠNG THỨC ĐƯỢC TRIỂN KHAI ĐỂ HỖ TRỢ DEVERLOPER.

Trang 16/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

3.2 Các quy tắc khai báo đối tượng / class

● Tên class phải viết BẰNG TIẾNG ANH


class AdministrativeProcedure {
AdministrativeProcedure({
required this.linhVuc,
required this.linhVucId,
required this.maLinhVuc,
required this.selected,
required this.countTthc,
required this.linhVucCapHuyen,
required this.linhVucCapXa,
required this.linhVucCapTinh,
});

final String linhVuc;


final String linhVucId;
final String maLinhVuc;
final bool selected;
final int countTthc;
final bool linhVucCapHuyen;
final bool linhVucCapXa;
final bool linhVucCapTinh;

● Tên các thuộc tính ĐƯỢC LẤY TRỰC TIẾP TỪ ĐỊNH NGHĨA CỦA BACK-END,
CHỈNH SỬA TỪ SNAKE_CASE (ten_thuoc_tinh) SANG camelCase
(linhVucCapHuyen). Để tiết kiệm thời gian, tôi sử dụng QuickType tại
https://app.quicktype.io/ với cài đặt như sau:

Tuy nhiên sau này sẽ buộc phải Apply freezed_annotation để giảm dung lược source
Code trên GIT, điều này được đồng nghiệp của tôi trong dự án thực hiện và sẽ cập
nhật mô tả vào tài liệu này.
● Các class/ object chỉ dùng để đọc dữ liệu thì các props trong đó phải là final,
các class dùng để nhập dữ liệu thì các props không cần là final:

Trang 17/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

class AdministrativeProcedureSearchForm {
AdministrativeProcedureSearchForm({
this.linhVucId = "",
this.mucDo = null,
this.capDonVi = null,
this.loaiTthc = null,
required this.textSearch,
this.pageSize = 20,
this.pageNumber = 1,
});

String? linhVucId;
int? mucDo;
int? capDonVi;
int? loaiTthc;
String? textSearch;
int? pageSize;
int? pageNumber;

3.3 Các partens được triển khai để hỗ trợ bảo trì, mở rộng, phát triển dự án

● Navigations:

Đây là nơi chứa toàn bộ những gì liên quan đến routes - navigation của dự án, bao
gồm observer, routes và animation/transition.

class MyApp extends StatefulWidget {


const MyApp({Key? key}) : super(key: key);

@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override

Trang 18/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Sizer(
builder: ((context, orientation, deviceType) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: 'Inter'),
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
);
},
navigatorKey: navigatorKey,
navigatorObservers: [
AppNavigatorObserver(),
],
initialRoute: Routes.ROOT,
onGenerateRoute: (settings) {
return AppNavigator().getRoute(settings);
},
home: SplashLoadingPage(),
);
}),
);
}
}
App sử dụng navigatorKey nhằm đưa content ra ngoài (lấy ý tưởng từ getX), và truyền
params vào class thông qua arguments.
class AppNavigator extends RouteObserver<PageRoute<dynamic>> {
Route<dynamic>? getRoute(RouteSettings settings) {
Map<String, dynamic>? arguments = _getArguments(settings);

switch (settings.name) {
case Routes.ROOT:
return _buildRoute(settings, const HomePage());
case Routes.REGISTER_OTP:
return _buildRoute(
settings,
RegisterOTPPage(
phoneNumber: arguments!['phoneNumber'],
username: arguments['username'],
registerType: arguments['registerType'],
));

_buildRoute(
RouteSettings routeSettings,
Widget builder,
){
return AppMaterialPageRoute(
builder: (context) => builder,
settings: routeSettings,
);
}

_getArguments(RouteSettings settings) {
return settings.arguments;
}

Trang 19/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

static Future push<T>(


String route, {
Object? arguments,
}) {
return state.pushNamed(route, arguments: arguments);
}

static Future pushNamedAndRemoveUntil<T>(


String route, {
Object? arguments,
}) {
return state.pushNamedAndRemoveUntil(
route,
(route) => false,
arguments: arguments,
);
}

static Future replaceWith<T>(


String route, {
Map<String, dynamic>? arguments,
}) {
return state.pushReplacementNamed(route, arguments: arguments);
}

static void popUntil<T>(String route) {


state.popUntil(ModalRoute.withName(route));
}

static void pop() {


if (canPop) {
state.pop();
}
}

static bool get canPop => state.canPop();

static String? currentRoute() => AppNavigatorObserver.currentRouteName;

static BuildContext? get context => navigatorKey.currentContext;

static NavigatorState get state => navigatorKey.currentState!;


}

Và nhờ vậy, ta sẽ có chung một context cho toàn app, không cần phải truyền content
vào các hàm Navigation, chi tiết về BuildContext được mô tả tại
https://api.flutter.dev/flutter/widgets/BuildContext-class.html

Ta có thể triển khai các Navigator Methods dưới dạng Static method như sau:
ButtonPrimary(
onTap: () {
AppNavigator.pushNamedAndRemoveUntil(routeName);
},
text: buttonContent,
),

Thay vì

Trang 20/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

ButtonPrimary(
onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil(routeName, (route) => false);
},
text: buttonContent,
)

● BLOC State management:

Ta sử dụng flutter_bloc: ^8 trong dự án, được mô tả tại với cách triển khai
như sau:
o 1. Mỗi một luồng tính năng chỉ có duy nhất một Bloc quản lý.
o 2. Không phân chia quá nhiều State, chỉ có một State tổng và phân
tách với nhau bằng các Enum Status
o 3. Các bloc được khởi tạo từ đầu và chạy xuyên suốt dự án (trong thời
điểm người viết viết ra tài liệu này, mọi thay đổi sau này sẽ được cập
nhật sau đó).

Cụ thể như sau:

Future<void> main() async {


WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await LocalStorageApp.adapter();
await AppLocalStorage.initialBox();
await BGeServiceDeviceInfor().initPlatformState();
Bloc.observer = AppBlocObserver();
runApp(MultiBlocProvider(providers: AppBlocs.blocProviders, child: MyApp()));
}

Bloc sẽ được khởi tạo ngay khi mở app, sử dụng Singleton để đảm bảo chỉ trỏ đến
DUY NHẤT MỘT OBJECT nhằm bảo toàn data trong suốt quá trình run app.

class AppBlocs {
/// Singleton Factory
static final AppBlocs _instance = AppBlocs._internal();

factory AppBlocs() {
return _instance;
}

AppBlocs._internal();

static final homeBloc = HomeBloc();


static final submitAdministrativeProcedureBloc =
SubmitAdministrativeProcedureBloc();
static final detailsAdministrativeBloc = DetailAdministrativeBloc();

Trang 21/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

static final AuthBloc authBloc = AuthBloc();


static final OtpBloc otpBloc = OtpBloc();
static final SearchAdministrativeProceduresBloc
searchAdministrativeProceduresBloc = SearchAdministrativeProceduresBloc();
static final UserProfBloc userProfBloc = UserProfBloc();

// BLOC Providers
static final List<BlocProvider> blocProviders = [
BlocProvider<HomeBloc>(create: (_) => homeBloc),
BlocProvider<SubmitAdministrativeProcedureBloc>(
create: (_) => submitAdministrativeProcedureBloc),
BlocProvider<DetailAdministrativeBloc>(
create: (_) => detailsAdministrativeBloc),
BlocProvider<AuthBloc>(create: (_) => authBloc),
BlocProvider<OtpBloc>(create: (_) => otpBloc),
BlocProvider<SearchAdministrativeProceduresBloc>(
create: (_) => searchAdministrativeProceduresBloc),
BlocProvider<UserProfBloc>(create: (_) => userProfBloc),
];

Cụ thể như sau:

1. State
enum StateStatus {
loading,
success,
}

enum SearchStatus {
popular,
advanced,
nornmal,
}

class SearchAdministrativeProceduresState extends Equatable {


const SearchAdministrativeProceduresState({
required this.nameSuggestions,
required this.sectorSuggestions,
required this.historySuggestions,
required this.searchResult,
required this.searchForm,
required this.hasReachedMax,
required this.status,
required this.searchStatus,
required this.sector,
});
final List<String> nameSuggestions;
final List<String> sectorSuggestions;
final List<String> historySuggestions;

final List<AdministrativeProcedureDetails> searchResult;


final AdministrativeProcedureSearchForm searchForm;
final bool hasReachedMax;
final StateStatus status;
final SearchStatus searchStatus;
final String sector;

Trang 22/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

Như đã nói bên trên, CHỈ CÓ MỘT STATE cho mỗi feature, ta phân tách các State
bằng các enum value như trên, để phân biệt 2 State, ta triển khai getter props => []
và phương thức copyWith như sau.

@override
List<Object> get props => [
nameSuggestions,
sectorSuggestions,
historySuggestions,
searchResult,
searchForm,
hasReachedMax,
status,
searchStatus,
sector,
];

SearchAdministrativeProceduresState copyWith({
List<String>? nameSuggestions,
List<String>? sectorSuggestions,
List<String>? historySuggestions,
List<AdministrativeProcedureDetails>? searchResult,
AdministrativeProcedureSearchForm? searchForm,
bool? hasReachedMax,
StateStatus? status,
SearchStatus? searchStatus,
String? sector,
}) {
return SearchAdministrativeProceduresState(
nameSuggestions: nameSuggestions ?? this.nameSuggestions,
sectorSuggestions: sectorSuggestions ?? this.sectorSuggestions,
historySuggestions: historySuggestions ?? this.historySuggestions,
searchResult: searchResult ?? this.searchResult,
searchForm: searchForm ?? this.searchForm,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
status: status ?? this.status,
searchStatus: searchStatus ?? this.searchStatus,
sector: sector ?? this.sector,
);
}
}

Trang 23/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

2. Events:
Áp dụng tương tự như trong DOCS của bloc tại đây:
https://bloclibrary.dev/#/gettingstarted

abstract class SearchAdministrativeProceduresEvent extends Equatable {


const SearchAdministrativeProceduresEvent();

@override
List<Object> get props => [];
}

class GetSearchHistoryEvent extends SearchAdministrativeProceduresEvent {}

class UpdateSearchHistoryEvent extends SearchAdministrativeProceduresEvent {


final List<String> newHistory;
UpdateSearchHistoryEvent({
required this.newHistory,
});
@override
List<Object> get props => [newHistory];
}

3. Bloc:
class SearchAdministrativeProceduresBloc extends Bloc<
SearchAdministrativeProceduresEvent, SearchAdministrativeProceduresState> {
SearchAdministrativeProceduresBloc()
: super(SearchAdministrativeProceduresState(
nameSuggestions: [],
sectorSuggestions: [],
historySuggestions: [],
searchResult: [],
searchForm: AdministrativeProcedureSearchForm(textSearch: ""),
hasReachedMax: false,
status: StateStatus.loading,
searchStatus: SearchStatus.popular,
sector: "",
)) {
// Remote
on<GetNameSuggestionString>(_getNameSuggestionString);
on<GetSectorsSuggestionString>(_getSectorsSuggestionString);
on<GetPopularAdministrativeProcedures>(_getPopularAdministrativeProcedures);
on<GetAdministrativeProceduresByTextSearch>(
_getAdministrativeProceduresByTextSearch);

Ta init bloc tại khởi điểm khởi tạo trong mục triển khai Bloc bên trên, với các initial
value nằm trong : super();
Dưới đây là cách triển khai các Function:

Future<void> _getNameSuggestionString(GetNameSuggestionString event,


Emitter<SearchAdministrativeProceduresState> emitter) async {
final _result =
await AdministrativeProceduresRepoImpl().getSuggestionNames();

Trang 24/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

if (_result is List<String>) {
emitter(state.copyWith(
nameSuggestions: _result,
status: StateStatus.success,
));
}
return;
}

Future<void> _getSectorsSuggestionString(GetSectorsSuggestionString event,


Emitter<SearchAdministrativeProceduresState> emitter) async {
final _result =
await AdministrativeProceduresRepoImpl().getSuggestionSectors();
if (_result is List<String>) {
emitter(state.copyWith(
sectorSuggestions: _result,
status: StateStatus.success,
));
}
return;
}

Các asynchronous Function được gọi tới tầng


domain/repositories/repository_name_impl.dart nhằm lấy Model và đưa tới UI bằng
emiiter.

4. UI:
BlocBuilder<SearchAdministrativeProceduresBloc,
SearchAdministrativeProceduresState>(
builder: (context, state) {
return Container(
height: 264,
child: _controller.text.isEmpty
? Column(
children: [
state.historySuggestions.isNotEmpty
? Container(
height: 40,
width: 100.w,
padding: EdgeInsets.symmetric(
horizontal:
DEFAULT_HORIZONTAL_PADDING),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Gần đây",
style: AppTextStyles.body
.fw600()
.fcPr10(),
),
GestureDetector(
onTap: removeAllSearchHistory,
child: Container(
height: 44,
alignment:
Alignment.centerRight,
child: Text(

Trang 25/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

"Xoá tất cả",


style: AppTextStyles.small
.fw600()
.fcPr6(),
),
),
)
],
),
)
: SizedBox(),
Container(
height: 220,
child: SuggestionList(
controller: _controller,
onSearch: onSearch,
suggestionsStrings:
state.historySuggestions,
),
)
],
)
: Container(
height: 220,
child: SuggestionList(
controller: _controller,
onSearch: onSearch,
suggestionsStrings: state.nameSuggestions
.where((element) => element
.toLowerCase()
.contains(
_controller.text.toLowerCase()))
.toList(),
),
),
);
},
),
],

Widget chủ yếu được dùng là BlocBuilder, như đã nói ở trên, chỉ có DUY NHẤT
MỘT STATE cho mỗi bloc, nên ta có thể rút ngắn các đoạn Code if … else rất nhi ều
trong các file UI, tuy nhiên cần quản lý và clear data thật kỹ để sử dụng, với mặt b ằng
trình độ của các thành viên trong đội ngũ phát triển thời điểm hiện tại thì hoàn toàn
hợp lý.

****

Trang 26/27
CÔNG TY CỔ PHẦN CÔNG NGHỆ SAVIS

4. HƯỚNG DẪN CLONE SOURCE CODE VÀ BUILD APP LẦN ĐẦU


4.1 Chuẩn bị

- Home brew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

- Git
brew install git

- Các quyền hạn liên quan về source git, tài khoản Apple Dev do công ti cung cấp.

- Cocoapods
sudo gem install cocoapods

4.2 Build app lần đầu

- Clone source code


Git clone https://gitlab.com/medychain/egov-dvc/app.git

- Clean flutter
Flutter clean

- Pub get các thư viện


Flutter pubget

- Generate các file gen


Flutter packages pub run build_runner build –delete-cònlicting-outputs

- Run flutter và xử lý các lỗi phát sinh

Trang 27/27

You might also like