การสร้างต้นแบบ Flutter เป็นเรื่องง่าย การสร้างแอปพลิเคชัน Flutter ที่พร้อมสำหรับการใช้งานจริงที่ปรับขนาด ทำงานได้ดีภายใต้โหลด และสามารถบำรุงรักษาได้เป็นเวลาหลายปี จำเป็นต้องมีความเข้าใจที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับสถาปัตยกรรม การจัดการสถานะ การทดสอบ และเวิร์กโฟลว์การปรับใช้ คู่มือนี้เชื่อมช่องว่างระหว่างโปรเจ็กต์บทช่วยสอนและแอปพลิเคชันในโลกแห่งความเป็นจริง โดยครอบคลุมรูปแบบและแนวทางปฏิบัติที่ทีมงาน Flutter มืออาชีพพึ่งพาทุกวัน
Clean Architecture สำหรับ Flutter
Clean architecture แยกแอปพลิเคชันของคุณออกเป็นเลเยอร์ที่แตกต่างกันโดยมีขอบเขตที่ชัดเจนและกฎการพึ่งพา การแยกนี้ทำให้โค้ดของคุณสามารถทดสอบได้ บำรุงรักษาได้ และเป็นอิสระจากเฟรมเวิร์กและเครื่องมือภายนอก
โครงสร้างเลเยอร์
แอป Flutter ที่ใช้งานจริงมักจะเป็นไปตามสถาปัตยกรรมสามชั้น:
- Presentation Layer- วิดเจ็ต เพจ และการจัดการสถานะ เลเยอร์นี้ขึ้นอยู่กับเลเยอร์โดเมนแต่ไม่เคยขึ้นอยู่กับแหล่งข้อมูลโดยตรง
- Domain Layer- ตรรกะทางธุรกิจ เอนทิตี และกรณีการใช้งาน เลเยอร์นี้ไม่มีการพึ่งพา Flutter หรือแพ็คเกจภายนอกใดๆ โดยกำหนดอินเทอร์เฟซพื้นที่เก็บข้อมูล (คลาสนามธรรม) ที่ชั้นข้อมูลนำไปใช้
- Data Layer- การใช้งานพื้นที่เก็บข้อมูล, ไคลเอนต์ API, การเข้าถึงฐานข้อมูลท้องถิ่น และโมเดลข้อมูล (DTO) เลเยอร์นี้ใช้อินเทอร์เฟซที่กำหนดไว้ในเลเยอร์โดเมน
lib/
core/
error/
exceptions.dart
failures.dart
network/
network_info.dart
usecases/
usecase.dart
features/
authentication/
data/
datasources/
auth_remote_datasource.dart
auth_local_datasource.dart
models/
user_model.dart
repositories/
auth_repository_impl.dart
domain/
entities/
user.dart
repositories/
auth_repository.dart
usecases/
login.dart
register.dart
logout.dart
presentation/
bloc/
auth_bloc.dart
auth_event.dart
auth_state.dart
pages/
login_page.dart
register_page.dart
widgets/
login_form.dartกฎการพึ่งพานั้นเข้มงวด: ชั้นในไม่เคยรู้เกี่ยวกับชั้นนอก เลเยอร์โดเมนกำหนดอินเทอร์เฟซพื้นที่เก็บข้อมูลเชิงนามธรรม และชั้นข้อมูลจัดให้มีการใช้งานที่เป็นรูปธรรม การกลับกันของการควบคุมนี้ทำให้คุณสามารถสลับแหล่งข้อมูลโดยไม่ต้องสัมผัสตรรกะทางธุรกิจ
State Management: BLoC และ Riverpod
การเลือกโซลูชันการจัดการสถานะที่เหมาะสมเป็นหนึ่งในการตัดสินใจทางสถาปัตยกรรมที่มีผลกระทบมากที่สุดในโปรเจ็กต์ Flutter
BLoC Pattern
BLoC (Business Logic Component) ใช้สตรีมเพื่อจัดการสถานะ เหตุการณ์ไหลเข้า รัฐไหลออก การไหลของข้อมูลแบบทิศทางเดียวนี้ทำให้สามารถคาดการณ์การเปลี่ยนแปลงสถานะได้และแก้ไขจุดบกพร่องได้ง่าย
// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
class LogoutRequested extends AuthEvent {}
// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final LoginUseCase loginUseCase;
final LogoutUseCase logoutUseCase;
AuthBloc({
required this.loginUseCase,
required this.logoutUseCase,
}) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
final result = await loginUseCase(
LoginParams(email: event.email, password: event.password),
);
result.fold(
(failure) => emit(AuthError(failure.message)),
(user) => emit(AuthAuthenticated(user)),
);
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
await logoutUseCase();
emit(AuthInitial());
}
}Riverpod
Riverpod นำเสนอแนวทางการจัดการสถานะที่ยืดหยุ่นและปลอดภัยในการคอมไพล์มากขึ้น ไม่เหมือนกับผู้ให้บริการ Riverpod ไม่ได้ขึ้นอยู่กับแผนผังวิดเจ็ต ทำให้ง่ายต่อการทดสอบและเขียน
// Define providers
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepositoryImpl(
remoteDatasource: ref.read(authRemoteDatasourceProvider),
localDatasource: ref.read(authLocalDatasourceProvider),
);
});
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.read(authRepositoryProvider));
});
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _repository;
AuthNotifier(this._repository) : super(const AuthState.initial());
Future<void> login(String email, String password) async {
state = const AuthState.loading();
final result = await _repository.login(email, password);
state = result.fold(
(failure) => AuthState.error(failure.message),
(user) => AuthState.authenticated(user),
);
}
}
// Use in widgets
class LoginPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
return authState.when(
initial: () => LoginForm(),
loading: () => const CircularProgressIndicator(),
authenticated: (user) => HomePage(user: user),
error: (message) => ErrorDisplay(message: message),
);
}
}การพึ่งพาการฉีด
การพึ่งพาอย่างเหมาะสมเป็นสิ่งจำเป็นสำหรับโค้ดที่ทดสอบได้ แพ็คเกจget_itมีตัวระบุตำแหน่งบริการที่เรียบง่ายซึ่งทำงานได้ดีกับสถาปัตยกรรมที่สะอาด
final sl = GetIt.instance;
void initDependencies() {
// External
sl.registerLazySingleton(() => Dio()..interceptors.add(AuthInterceptor()));
sl.registerLazySingleton(() => InternetConnectionChecker());
// Data sources
sl.registerLazySingleton<AuthRemoteDatasource>(
() => AuthRemoteDatasourceImpl(dio: sl()),
);
sl.registerLazySingleton<AuthLocalDatasource>(
() => AuthLocalDatasourceImpl(secureStorage: sl()),
);
// Repositories
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDatasource: sl(),
localDatasource: sl(),
networkInfo: sl(),
),
);
// Use cases
sl.registerLazySingleton(() => LoginUseCase(sl()));
sl.registerLazySingleton(() => RegisterUseCase(sl()));
// BLoCs
sl.registerFactory(() => AuthBloc(
loginUseCase: sl(),
logoutUseCase: sl(),
));
}API การบูรณาการกับ Dio
Dio เป็นไคลเอนต์ HTTP ที่ได้รับความนิยมมากที่สุดสำหรับ Dart โดยนำเสนอตัวดักรับ การกำหนดค่าส่วนกลาง และการรองรับ FormData จัดโครงสร้างเลเยอร์ API ของคุณด้วยการจัดการคำขอและการตอบสนองที่ปลอดภัยต่อประเภท
class ApiClient {
final Dio _dio;
ApiClient(this._dio) {
_dio.options = BaseOptions(
baseUrl: Environment.apiBaseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
);
_dio.interceptors.addAll([
AuthInterceptor(),
LogInterceptor(requestBody: true, responseBody: true),
RetryInterceptor(dio: _dio, retries: 3),
]);
}
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
required T Function(dynamic data) parser,
}) async {
try {
final response = await _dio.get(path, queryParameters: queryParameters);
return parser(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
AppException _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.receiveTimeout:
return NetworkException('Connection timed out');
case DioExceptionType.badResponse:
return ServerException(
error.response?.statusCode ?? 500,
error.response?.data?['message'] ?? 'Unknown error',
);
default:
return NetworkException('Network error occurred');
}
}
}พื้นที่จัดเก็บในตัวเครื่องพร้อม Hive และ Sqflite
แอปที่ใช้งานจริงส่วนใหญ่ต้องการความคงอยู่ของข้อมูลในเครื่อง เลือกเครื่องมือที่เหมาะสมตามความซับซ้อนของข้อมูลของคุณ
Hive สำหรับ Key-Value และ Object Storage
Hive เป็นฐานข้อมูล NoSQL ที่มีน้ำหนักเบาและรวดเร็วเป็นพิเศษซึ่งเขียนด้วย Dart ล้วนๆ เหมาะอย่างยิ่งสำหรับการแคช การกำหนดค่าตามความชอบของผู้ใช้ และการจัดเก็บชุดข้อมูลขนาดเล็กถึงขนาดกลาง
@HiveType(typeId: 0)
class CachedArticle extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String content;
@HiveField(3)
final DateTime cachedAt;
CachedArticle({
required this.id,
required this.title,
required this.content,
required this.cachedAt,
});
}
class ArticleCacheService {
static const _boxName = 'articles_cache';
Future<void> cacheArticles(List<Article> articles) async {
final box = await Hive.openBox<CachedArticle>(_boxName);
final cached = articles.map((a) => CachedArticle(
id: a.id,
title: a.title,
content: a.content,
cachedAt: DateTime.now(),
));
await box.clear();
await box.addAll(cached);
}
Future<List<CachedArticle>> getCachedArticles() async {
final box = await Hive.openBox<CachedArticle>(_boxName);
return box.values.toList();
}
}Sqflite สำหรับข้อมูลเชิงสัมพันธ์
เมื่อข้อมูลของคุณมีความสัมพันธ์ที่ซับซ้อน และคุณต้องการคำสั่ง SQL Sqflite จะจัดเตรียมการใช้งาน SQLite เต็มรูปแบบสำหรับ Flutter ใช้สำหรับข้อมูลที่มีโครงสร้างซึ่งได้ประโยชน์จากการรวม จัดทำดัชนี และธุรกรรม
การแจ้งเตือนแบบพุช
ใช้การแจ้งเตือนแบบพุชโดยใช้ Firebase Cloud Messaging (FCM) พร้อมการจัดการสิทธิ์ที่เหมาะสมและการประมวลผลข้อความพื้นหลัง
class NotificationService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
Future<void> initialize() async {
// Request permission
final settings = await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
// Get FCM token
final token = await _messaging.getToken();
await _sendTokenToServer(token);
// Listen for token refresh
_messaging.onTokenRefresh.listen(_sendTokenToServer);
// Handle foreground messages
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// Handle background/terminated message taps
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageTap);
}
}
void _handleForegroundMessage(RemoteMessage message) {
// Show local notification using flutter_local_notifications
FlutterLocalNotificationsPlugin().show(
message.hashCode,
message.notification?.title,
message.notification?.body,
const NotificationDetails(
android: AndroidNotificationDetails(
'default_channel',
'Default',
importance: Importance.high,
),
),
);
}
}Deep Linking
Deep Linking ช่วยให้ผู้ใช้สามารถนำทางไปยังเนื้อหาเฉพาะภายในแอปของคุณได้โดยตรงจาก URL ภายนอก Flutter รองรับทั้งลิงก์ในรายละเอียดที่ใช้ URI และลิงก์แบบไดนามิก
// Configure in MaterialApp
MaterialApp(
onGenerateRoute: (settings) {
final uri = Uri.parse(settings.name ?? '');
if (uri.pathSegments.first == 'product') {
final productId = uri.pathSegments[1];
return MaterialPageRoute(
builder: (_) => ProductDetailPage(id: productId),
);
}
if (uri.pathSegments.first == 'order') {
final orderId = uri.pathSegments[1];
return MaterialPageRoute(
builder: (_) => OrderTrackingPage(id: orderId),
);
}
return MaterialPageRoute(builder: (_) => const HomePage());
},
)สำหรับ Deep Link ที่แข็งแกร่งยิ่งขึ้น ให้ใช้แพ็คเกจgo_routerซึ่งให้การกำหนดเส้นทางที่ประกาศพร้อมการสนับสนุน Deep Link การเปลี่ยนเส้นทาง และการนำทางแบบซ้อน
CI/CD พร้อม Codemagic และ Fastlane
ไปป์ไลน์การสร้างและปรับใช้อัตโนมัติถือเป็นสิ่งสำคัญสำหรับแอปที่ใช้งานจริง Codemagic ให้บริการ CI/CD แบบเนทีฟ Flutter ในขณะที่ Fastlane นำเสนอระบบอัตโนมัติที่ปรับแต่งได้มากขึ้น
การกำหนดค่า Codemagic
# codemagic.yaml
workflows:
production-release:
name: Production Release
max_build_duration: 60
environment:
flutter: stable
vars:
APP_STORE_CONNECT_KEY_ID: Encrypted(...)
GOOGLE_PLAY_SERVICE_ACCOUNT: Encrypted(...)
scripts:
- name: Install dependencies
script: flutter pub get
- name: Run tests
script: flutter test --coverage
- name: Build Android
script: flutter build appbundle --release
- name: Build iOS
script: |
flutter build ipa --release \
--export-options-plist=/path/to/ExportOptions.plist
artifacts:
- build/**/outputs/**/*.aab
- build/ios/ipa/*.ipa
publishing:
google_play:
credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT
track: internal
app_store_connect:
api_key: $APP_STORE_CONNECT_KEY_IDFastlane Integration
Fastlane ให้การควบคุมโดยละเอียดเหนือกระบวนการสร้างและการส่ง กำหนดช่องทางสำหรับระยะการเปิดตัวที่แตกต่างกัน:
# fastlane/Fastfile
platform :ios do
desc "Deploy to TestFlight"
lane :beta do
build_flutter_app(target: "lib/main.dart")
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
desc "Deploy to App Store"
lane :release do
build_flutter_app(target: "lib/main.dart")
upload_to_app_store(
submit_for_review: true,
automatic_release: false
)
end
endการทำโปรไฟล์ประสิทธิภาพ
แอปการผลิตต้องการประสิทธิภาพที่สม่ำเสมอ Flutter DevTools มอบความสามารถในการสร้างโปรไฟล์ที่ครอบคลุม
- Widget การติดตามการสร้างใหม่- ใช้การซ้อนทับประสิทธิภาพและ DevTools เพื่อระบุวิดเจ็ตที่สร้างขึ้นใหม่มากเกินไป ใช้ตัวสร้าง
constและการจัดการสถานะแบบเลือกเพื่อลดการสร้างใหม่ - การเรนเดอร์เฟรม- ตรวจสอบมุมมองไทม์ไลน์เพื่อให้แน่ใจว่าเฟรมเรนเดอร์ภายใน 16 มิลลิวินาที (60fps) หรือ 8 มิลลิวินาที (120fps) มองหาขั้นตอนการสร้าง เค้าโครง และทาสีที่มีราคาแพง
- การทำโปรไฟล์หน่วยความจำ- ติดตามการจัดสรรหน่วยความจำเพื่อตรวจจับการรั่วไหล ผู้ร้ายที่พบบ่อย ได้แก่ การสมัครสมาชิกสตรีมที่ยังไม่ยกเลิก ตัวควบคุมที่ไม่ถูกเปิดเผย และการอ้างอิงที่เก็บไว้ในการปิด
- ประสิทธิภาพการเริ่มต้น- เลื่อนการเริ่มต้นจำนวนมากโดยใช้
WidgetsBinding.instance.addPostFrameCallbackใช้การโหลดแบบเลื่อนด้วยการนำเข้าdeferred asสำหรับฟีเจอร์ที่ไม่จำเป็นในทันที
// Profile-mode build for accurate performance measurement
// flutter run --profile
// Add performance overlay in debug builds
MaterialApp(
showPerformanceOverlay: true,
// ...
)บทสรุป
การสร้างแอปพลิเคชัน Flutter ที่พร้อมสำหรับการผลิตต้องการมากกว่าแค่การรู้แค็ตตาล็อกวิดเจ็ต ต้องการสถาปัตยกรรมที่รอบคอบ การจัดการสถานะที่แข็งแกร่ง การทดสอบที่ครอบคลุม และไปป์ไลน์การปรับใช้อัตโนมัติ ด้วยการนำสถาปัตยกรรมที่สะอาดมาใช้ การลงทุนในการฉีดการขึ้นต่อกันที่เหมาะสม การใช้การจัดการข้อผิดพลาดอย่างละเอียด และการสร้างเวิร์กโฟลว์ CI/CD คุณจึงสร้างแอปพลิเคชันที่ไม่เพียงแต่ใช้งานได้เท่านั้น แต่ยังบำรุงรักษาและปรับขนาดได้ในระยะยาว
เริ่มต้นด้วยการสร้างสถาปัตยกรรมของคุณแต่เนิ่นๆ เขียนการทดสอบตั้งแต่วันแรก และทำให้ไปป์ไลน์การปรับใช้ของคุณเป็นแบบอัตโนมัติก่อนการเปิดตัวครั้งแรก การลงทุนล่วงหน้าเหล่านี้ทบต้นเมื่อเวลาผ่านไป ช่วยให้ทีมของคุณสามารถจัดส่งคุณสมบัติต่างๆ ได้เร็วขึ้น โดยมีการถดถอยน้อยลง และมั่นใจมากขึ้นในทุกการเปิดตัว