ข้ามไปยังเนื้อหา

บล็อก

การเรียนรู้ Flutter: การสร้างแอปพลิเคชันบนมือถือที่พร้อมสำหรับการผลิต

คู่มือฉบับสมบูรณ์ในการสร้างแอปพลิเคชันมือถือที่แข็งแกร่งและปรับขนาดได้ด้วย Flutter และ Dart

  • อ่าน 6 นาที
  • Updated

การสร้างต้นแบบ Flutter เป็นเรื่องง่าย การสร้างแอปพลิเคชัน Flutter ที่พร้อมสำหรับการใช้งานจริงที่ปรับขนาด ทำงานได้ดีภายใต้โหลด และสามารถบำรุงรักษาได้เป็นเวลาหลายปี จำเป็นต้องมีความเข้าใจที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับสถาปัตยกรรม การจัดการสถานะ การทดสอบ และเวิร์กโฟลว์การปรับใช้ คู่มือนี้เชื่อมช่องว่างระหว่างโปรเจ็กต์บทช่วยสอนและแอปพลิเคชันในโลกแห่งความเป็นจริง โดยครอบคลุมรูปแบบและแนวทางปฏิบัติที่ทีมงาน Flutter มืออาชีพพึ่งพาทุกวัน

Clean Architecture สำหรับ Flutter

Clean architecture แยกแอปพลิเคชันของคุณออกเป็นเลเยอร์ที่แตกต่างกันโดยมีขอบเขตที่ชัดเจนและกฎการพึ่งพา การแยกนี้ทำให้โค้ดของคุณสามารถทดสอบได้ บำรุงรักษาได้ และเป็นอิสระจากเฟรมเวิร์กและเครื่องมือภายนอก

Flutter สถาปัตยกรรมเลเยอร์รหัส Dart ของคุณตรรกะทางธุรกิจ & amp; องค์ประกอบ UIFlutter กรอบงานวิดเจ็ตการแสดงผลFoundation (ท่าทาง, ภาพเคลื่อนไหว)Engine (C ++)Skia กราฟิก • รันไทม์ Dart • ช่องทางแพลตฟอร์มiOS (UIKit)ARM / Apple SiliconAndroid (NDK)ARM / x86

โครงสร้างเลเยอร์

แอป 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มีตัวระบุตำแหน่งบริการที่เรียบง่ายซึ่งทำงานได้ดีกับสถาปัตยกรรมที่สะอาด

Flutter Widget TreeMaterialAppนั่งร้านคอลัมน์AppBarรายการดูFABรายการไทล์ 1ListTile 2
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_ID

Fastlane 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 คุณจึงสร้างแอปพลิเคชันที่ไม่เพียงแต่ใช้งานได้เท่านั้น แต่ยังบำรุงรักษาและปรับขนาดได้ในระยะยาว

เริ่มต้นด้วยการสร้างสถาปัตยกรรมของคุณแต่เนิ่นๆ เขียนการทดสอบตั้งแต่วันแรก และทำให้ไปป์ไลน์การปรับใช้ของคุณเป็นแบบอัตโนมัติก่อนการเปิดตัวครั้งแรก การลงทุนล่วงหน้าเหล่านี้ทบต้นเมื่อเวลาผ่านไป ช่วยให้ทีมของคุณสามารถจัดส่งคุณสมบัติต่างๆ ได้เร็วขึ้น โดยมีการถดถอยน้อยลง และมั่นใจมากขึ้นในทุกการเปิดตัว