Przejdź do treści
Mobile

Flutter i Dart - Cross-platform development w praktyce

Opublikowano:
·6 min czytania·Autor: MDS Software Solutions Group

Flutter Dart Cross-platform

mobile

Flutter i Dart - Cross-platform development

Flutter to otwartozdrodlowy framework od Google, ktory umozliwia tworzenie natywnie kompilowanych aplikacji na urzadzenia mobilne, web i desktop z jednej bazy kodu. W polaczeniu z jezykiem Dart, Flutter oferuje wyjatkowe doswiadczenie deweloperskie, szybki cykl rozwoju dzieki Hot Reload oraz wydajnosc porownywalna z aplikacjami natywnymi. W tym kompleksowym przewodniku omowimy architekture Fluttera, jezyk Dart, system widgetow, zarzadzanie stanem, integracje z platformami natywnymi oraz najlepsze praktyki CI/CD.

Architektura Flutter#

Flutter wyroznia sie unikalna architektura, ktora odroznia go od innych frameworkow cross-platform. Zamiast korzystac z natywnych komponentow UI (jak React Native), Flutter renderuje kazdy piksel na ekranie za pomoca wlasnego silnika graficznego - Skia (a od niedawna Impeller).

Warstwy architektoniczne#

Architektura Fluttera sklada sie z trzech glownych warstw:

  • Framework (Dart) - najwyzsza warstwa zawierajaca widgety Material Design i Cupertino, system gestow, animacje, rendering i painting
  • Engine (C++) - silnik odpowiedzialny za renderowanie grafiki (Skia/Impeller), obsluge tekstu, siec, plugin system i Dart runtime
  • Embedder (platform-specific) - warstwa integrujaca silnik z konkretna platforma (Android, iOS, Windows, macOS, Linux)
// Przyklad struktury aplikacji Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MDS Flutter App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

Silnik renderujacy - Impeller#

Od Flutter 3.16 domyslnym silnikiem renderujacym na iOS jest Impeller, ktory zastepuje Skia. Impeller eliminuje problem kompilacji shaderow w runtime (tzw. "jank"), dzieki czemu animacje sa plynniejsze od pierwszego uruchomienia. Na Androidzie Impeller jest dostepny jako opcja preview.

Jezyk Dart - Fundament Flutter#

Dart to nowoczesny, obiektowy jezyk programowania stworzony przez Google. Zostal zaprojektowany z mysla o produktywnosci deweloperow i wydajnosci aplikacji.

Kluczowe cechy Dart#

// Null safety - domyslnie od Dart 2.12
String? nullableString; // moze byc null
String nonNullableString = 'Zawsze ma wartosc';

// Extension methods
extension StringExtension on String {
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }
}

// Pattern matching (Dart 3.0+)
sealed class Shape {}
class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}
class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
}

double area(Shape shape) => switch (shape) {
  Circle(radius: var r) => 3.14159 * r * r,
  Rectangle(width: var w, height: var h) => w * h,
};

// Records - lekkie krotki (Dart 3.0+)
(String, int) getUserInfo() => ('Jan Kowalski', 30);

// Async/Await
Future<List<User>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));
  if (response.statusCode == 200) {
    final List<dynamic> data = jsonDecode(response.body);
    return data.map((json) => User.fromJson(json)).toList();
  }
  throw Exception('Blad pobierania uzytkownikow');
}

// Streams
Stream<int> countStream(int max) async* {
  for (int i = 0; i < max; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

Kompilacja Dart#

Dart obsluguje dwa tryby kompilacji:

  • JIT (Just-In-Time) - uzywany podczas developmentu, umozliwia Hot Reload
  • AOT (Ahead-Of-Time) - uzywany w buildach produkcyjnych, generuje natywny kod maszynowy

Widget Tree - Serce Flutter#

W Flutter wszystko jest widgetem. Interfejs uzytkownika jest budowany jako drzewo widgetow, gdzie kazdy element UI jest reprezentowany przez obiekt klasy Widget.

StatelessWidget vs StatefulWidget#

// StatelessWidget - niemutowalny, nie posiada wewnetrznego stanu
class UserCard extends StatelessWidget {
  final String name;
  final String email;
  final String avatarUrl;

  const UserCard({
    super.key,
    required this.name,
    required this.email,
    required this.avatarUrl,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              radius: 30,
              backgroundImage: NetworkImage(avatarUrl),
            ),
            const SizedBox(width: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name, style: const TextStyle(
                  fontSize: 18, fontWeight: FontWeight.bold,
                )),
                const SizedBox(height: 4),
                Text(email, style: TextStyle(
                  color: Colors.grey[600], fontSize: 14,
                )),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// StatefulWidget - posiada mutowalny stan wewnetrzny
class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Licznik: $_counter', style: const TextStyle(fontSize: 24)),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Zwieksz'),
        ),
      ],
    );
  }
}

Cykl zycia widgetow#

StatefulWidget posiada bogaty cykl zycia, ktory pozwala na kontrolowanie zachowania widgetu w roznych fazach:

class LifecycleWidget extends StatefulWidget {
  const LifecycleWidget({super.key});

  @override
  State<LifecycleWidget> createState() => _LifecycleWidgetState();
}

class _LifecycleWidgetState extends State<LifecycleWidget> {
  @override
  void initState() {
    super.initState();
    // Wywoływane raz przy tworzeniu - inicjalizacja kontrolerow, subskrypcji
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Wywoływane gdy zmieniaja sie InheritedWidget-y
  }

  @override
  void didUpdateWidget(covariant LifecycleWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Wywoływane gdy rodzic przebudowuje widget z nowymi parametrami
  }

  @override
  void dispose() {
    // Czyszczenie zasobow - kontrolery, subskrypcje, timery
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

Material Design i Cupertino#

Flutter dostarcza dwa zestawy widgetow dopasowane do konwencji projektowych Androida i iOS.

Material Design 3#

class MaterialExample extends StatelessWidget {
  const MaterialExample({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF1A73E8),
          brightness: Brightness.light,
        ),
        typography: Typography.material2021(),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Material 3 App'),
          actions: [
            IconButton(
              icon: const Icon(Icons.search),
              onPressed: () {},
            ),
          ],
        ),
        body: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            FilledButton(
              onPressed: () {},
              child: const Text('Filled Button'),
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: () {},
              child: const Text('Outlined Button'),
            ),
            const SizedBox(height: 16),
            const SearchBar(hintText: 'Szukaj...'),
          ],
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: () {},
          icon: const Icon(Icons.add),
          label: const Text('Dodaj'),
        ),
        navigationBar: NavigationBar(
          destinations: const [
            NavigationDestination(icon: Icon(Icons.home), label: 'Start'),
            NavigationDestination(icon: Icon(Icons.explore), label: 'Odkryj'),
            NavigationDestination(icon: Icon(Icons.person), label: 'Profil'),
          ],
        ),
      ),
    );
  }
}

Cupertino (styl iOS)#

import 'package:flutter/cupertino.dart';

class CupertinoExample extends StatelessWidget {
  const CupertinoExample({super.key});

  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      theme: const CupertinoThemeData(
        primaryColor: CupertinoColors.activeBlue,
      ),
      home: CupertinoPageScaffold(
        navigationBar: const CupertinoNavigationBar(
          middle: Text('Cupertino App'),
        ),
        child: SafeArea(
          child: CupertinoListSection(
            header: const Text('Ustawienia'),
            children: [
              CupertinoListTile(
                title: const Text('Powiadomienia'),
                trailing: CupertinoSwitch(
                  value: true,
                  onChanged: (v) {},
                ),
              ),
              CupertinoListTile(
                title: const Text('Motyw ciemny'),
                trailing: CupertinoSwitch(
                  value: false,
                  onChanged: (v) {},
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Nawigacja w Flutter#

Flutter oferuje kilka podejsc do nawigacji, od prostego Navigator 1.0 po deklaratywny Router (Navigator 2.0) i popularne pakiety takie jak GoRouter.

// GoRouter - zalecane podejscie do nawigacji
import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'products',
          builder: (context, state) => const ProductsScreen(),
          routes: [
            GoRoute(
              path: ':id',
              builder: (context, state) {
                final id = state.pathParameters['id']!;
                return ProductDetailScreen(productId: id);
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) => const ProfileScreen(),
          redirect: (context, state) {
            final isLoggedIn = AuthService.instance.isLoggedIn;
            if (!isLoggedIn) return '/login';
            return null;
          },
        ),
      ],
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
  ],
);

Zarzadzanie stanem#

Zarzadzanie stanem to kluczowy aspekt kazdej aplikacji Flutter. Istnieje wiele rozwiazan, z ktorych kazde ma swoje zalety.

Provider#

Provider to najprostsze i oficjalnie rekomendowane rozwiazanie do zarzadzania stanem:

// Model danych z ChangeNotifier
class CartModel extends ChangeNotifier {
  final List<Product> _items = [];

  List<Product> get items => List.unmodifiable(_items);
  int get totalItems => _items.length;
  double get totalPrice =>
      _items.fold(0, (sum, item) => sum + item.price);

  void addItem(Product product) {
    _items.add(product);
    notifyListeners();
  }

  void removeItem(Product product) {
    _items.remove(product);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

// Konfiguracja Provider
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => CartModel()),
        ChangeNotifierProvider(create: (_) => UserModel()),
      ],
      child: const MyApp(),
    ),
  );
}

// Uzycie w widgecie
class CartScreen extends StatelessWidget {
  const CartScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<CartModel>(
      builder: (context, cart, child) {
        return Column(
          children: [
            Text('Produkty w koszyku: ${cart.totalItems}'),
            Text('Suma: ${cart.totalPrice.toStringAsFixed(2)} PLN'),
            Expanded(
              child: ListView.builder(
                itemCount: cart.items.length,
                itemBuilder: (context, index) {
                  final item = cart.items[index];
                  return ListTile(
                    title: Text(item.name),
                    subtitle: Text('${item.price} PLN'),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () => cart.removeItem(item),
                    ),
                  );
                },
              ),
            ),
          ],
        );
      },
    );
  }
}

Riverpod#

Riverpod to ewolucja Provider, ktora rozwiazuje wiele jego ograniczen - eliminuje zaleznosc od BuildContext i oferuje lepsze bezpieczenstwo typow:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Definicja providerow
final userRepositoryProvider = Provider((ref) => UserRepository());

final usersProvider = FutureProvider<List<User>>((ref) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchUsers();
});

final selectedUserProvider = StateProvider<User?>((ref) => null);

// Notifier dla zlozonej logiki
class TodoNotifier extends Notifier<List<Todo>> {
  @override
  List<Todo> build() => [];

  void addTodo(String title) {
    state = [...state, Todo(title: title, completed: false)];
  }

  void toggleTodo(int index) {
    state = [
      for (int i = 0; i < state.length; i++)
        if (i == index)
          state[i].copyWith(completed: !state[i].completed)
        else
          state[i],
    ];
  }
}

final todosProvider = NotifierProvider<TodoNotifier, List<Todo>>(
  TodoNotifier.new,
);

// Uzycie w widgecie
class UserListScreen extends ConsumerWidget {
  const UserListScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    return usersAsync.when(
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(users[index].name),
          onTap: () => ref.read(selectedUserProvider.notifier).state = users[index],
        ),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(child: Text('Blad: $error')),
    );
  }
}

Bloc (Business Logic Component)#

Bloc to wzorzec architektoniczny oparty na strumieniach (Streams), ktory oddziela logike biznesowa od warstwy prezentacji:

import 'package:flutter_bloc/flutter_bloc.dart';

// Events
sealed class AuthEvent {}
class LoginRequested extends AuthEvent {
  final String email, password;
  LoginRequested({required this.email, required this.password});
}
class LogoutRequested extends AuthEvent {}

// States
sealed 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 AuthRepository _authRepository;

  AuthBloc(this._authRepository) : super(AuthInitial()) {
    on<LoginRequested>(_onLogin);
    on<LogoutRequested>(_onLogout);
  }

  Future<void> _onLogin(LoginRequested event, Emitter<AuthState> emit) async {
    emit(AuthLoading());
    try {
      final user = await _authRepository.login(event.email, event.password);
      emit(AuthAuthenticated(user));
    } catch (e) {
      emit(AuthError(e.toString()));
    }
  }

  Future<void> _onLogout(LogoutRequested event, Emitter<AuthState> emit) async {
    await _authRepository.logout();
    emit(AuthInitial());
  }
}

Platform Channels - Integracja z kodem natywnym#

Platform Channels umozliwiaja komunikacje miedzy kodem Dart a kodem natywnym platformy (Kotlin/Java na Androidzie, Swift/Objective-C na iOS):

import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('com.mds.app/battery');

  static Future<int> getBatteryLevel() async {
    try {
      final int result = await _channel.invokeMethod('getBatteryLevel');
      return result;
    } on PlatformException catch (e) {
      throw Exception('Nie mozna odczytac poziomu baterii: ${e.message}');
    }
  }

  // EventChannel do ciaglego monitorowania
  static const _eventChannel = EventChannel('com.mds.app/battery_stream');

  static Stream<int> get batteryStream {
    return _eventChannel.receiveBroadcastStream().map((event) => event as int);
  }
}

// Uzycie w widgecie
class BatteryWidget extends StatelessWidget {
  const BatteryWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: BatteryService.batteryStream,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('Bateria: ${snapshot.data}%');
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

Flutter Web i Desktop#

Flutter nie ogranicza sie do aplikacji mobilnych. Framework wspiera rowniez platformy webowe i desktopowe.

Flutter Web#

Flutter Web kompiluje kod Dart do JavaScriptu (CanvasKit lub HTML renderer) i pozwala uruchamiac aplikacje w przegladarce:

import 'package:flutter/foundation.dart' show kIsWeb;

class PlatformAdaptiveWidget extends StatelessWidget {
  const PlatformAdaptiveWidget({super.key});

  @override
  Widget build(BuildContext context) {
    if (kIsWeb) {
      return const WebOptimizedLayout();
    }

    return switch (Theme.of(context).platform) {
      TargetPlatform.iOS || TargetPlatform.macOS => const CupertinoLayout(),
      TargetPlatform.windows || TargetPlatform.linux => const DesktopLayout(),
      _ => const MaterialLayout(),
    };
  }
}

Flutter Desktop#

Flutter Desktop umozliwia tworzenie natywnych aplikacji na Windows, macOS i Linux:

// Obsluga okien na desktop
import 'package:window_manager/window_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {
    await windowManager.ensureInitialized();
    WindowOptions windowOptions = const WindowOptions(
      size: Size(1280, 720),
      minimumSize: Size(800, 600),
      center: true,
      title: 'MDS Desktop App',
    );
    windowManager.waitUntilReadyToShow(windowOptions, () async {
      await windowManager.show();
      await windowManager.focus();
    });
  }

  runApp(const MyApp());
}

Hot Reload i Hot Restart#

Jedna z najwiekszych zalet Fluttera jest Hot Reload - mechanizm pozwalajacy na natychmiastowe odswiezanie zmian w kodzie bez utraty stanu aplikacji.

  • Hot Reload - wstrzykuje zaktualizowany kod zrodlowy do dzialajacego Dart VM, zachowujac stan aplikacji. Zmiany w UI sa widoczne w mniej niz sekunde
  • Hot Restart - restartuje aplikacje od nowa, resetujac caly stan. Potrzebny przy zmianach w initState, globalnych zmiennych czy strukturze klas

Hot Reload dziala dzieki kompilacji JIT (Just-In-Time), ktora jest uzywana w trybie debug. W buildach produkcyjnych Flutter korzysta z kompilacji AOT (Ahead-Of-Time), generujac natywny kod maszynowy dla maksymalnej wydajnosci.

Testowanie w Flutter#

Flutter oferuje kompleksowe narzedzia do testowania na trzech poziomach.

Testy jednostkowe#

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('CartModel', () {
    late CartModel cart;

    setUp(() {
      cart = CartModel();
    });

    test('powinien dodac produkt do koszyka', () {
      final product = Product(id: '1', name: 'Widget', price: 29.99);
      cart.addItem(product);

      expect(cart.totalItems, 1);
      expect(cart.totalPrice, 29.99);
    });

    test('powinien obliczyc laczna cene', () {
      cart.addItem(Product(id: '1', name: 'A', price: 10.0));
      cart.addItem(Product(id: '2', name: 'B', price: 20.0));

      expect(cart.totalPrice, 30.0);
    });

    test('powinien wyczyscic koszyk', () {
      cart.addItem(Product(id: '1', name: 'A', price: 10.0));
      cart.clear();

      expect(cart.totalItems, 0);
      expect(cart.totalPrice, 0.0);
    });
  });
}

Testy widgetow#

import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('CounterWidget powinien inkrementowac licznik', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: CounterWidget()));

    expect(find.text('Licznik: 0'), findsOneWidget);
    expect(find.text('Licznik: 1'), findsNothing);

    await tester.tap(find.text('Zwieksz'));
    await tester.pump();

    expect(find.text('Licznik: 0'), findsNothing);
    expect(find.text('Licznik: 1'), findsOneWidget);
  });

  testWidgets('UserCard powinien wyswietlac dane uzytkownika', (tester) async {
    await tester.pumpWidget(const MaterialApp(
      home: Scaffold(
        body: UserCard(
          name: 'Jan Kowalski',
          email: 'jan@example.com',
          avatarUrl: 'https://example.com/avatar.jpg',
        ),
      ),
    ));

    expect(find.text('Jan Kowalski'), findsOneWidget);
    expect(find.text('jan@example.com'), findsOneWidget);
  });
}

Testy integracyjne#

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('pelny przeplyw logowania', (tester) async {
    await tester.pumpWidget(const MyApp());

    await tester.enterText(find.byKey(const Key('email')), 'test@mds.pl');
    await tester.enterText(find.byKey(const Key('password')), 'password123');
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle();

    expect(find.text('Witaj, Test!'), findsOneWidget);
  });
}

CI/CD z Codemagic i Fastlane#

Automatyzacja procesu budowania i wdrazania jest kluczowa dla profesjonalnych projektow Flutter.

Codemagic#

Codemagic to dedykowana platforma CI/CD dla Flutter, ktora oferuje natywne wsparcie dla budowania aplikacji na wszystkie platformy:

# codemagic.yaml
workflows:
  production-release:
    name: Production Release
    max_build_duration: 60
    instance_type: mac_mini_m1
    environment:
      flutter: stable
      groups:
        - app_store_credentials
        - google_play_credentials
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: 'v*'
    scripts:
      - name: Instalacja zaleznosci
        script: flutter pub get
      - name: Analiza kodu
        script: flutter analyze
      - name: Testy jednostkowe
        script: flutter test
      - name: Build Android
        script: |
          flutter build appbundle --release \
            --build-number=$PROJECT_BUILD_NUMBER
      - name: Build iOS
        script: |
          flutter build ipa --release \
            --build-number=$PROJECT_BUILD_NUMBER \
            --export-options-plist=/Users/builder/export_options.plist
    artifacts:
      - build/**/outputs/**/*.aab
      - build/ios/ipa/*.ipa
    publishing:
      google_play:
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        track: production
      app_store_connect:
        api_key: $APP_STORE_CONNECT_PRIVATE_KEY
        key_id: $APP_STORE_CONNECT_KEY_ID
        issuer_id: $APP_STORE_CONNECT_ISSUER_ID

Fastlane#

Fastlane to popularne narzedzie do automatyzacji budowania, testowania i wdrazania aplikacji mobilnych:

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Wdrozenie na TestFlight"
  lane :beta do
    build_ios_app(
      scheme: "Runner",
      archive_path: "./build/Runner.xcarchive",
      export_method: "app-store",
      output_directory: "./build/Runner",
    )
    upload_to_testflight(skip_waiting_for_build_processing: true)
  end

  desc "Wdrozenie na App Store"
  lane :release do
    build_ios_app(scheme: "Runner", export_method: "app-store")
    upload_to_app_store(force: true, submit_for_review: true)
  end
end

platform :android do
  desc "Wdrozenie na Google Play (beta)"
  lane :beta do
    upload_to_play_store(
      track: 'beta',
      aab: '../build/app/outputs/bundle/release/app-release.aab',
    )
  end
end

Flutter vs React Native - Porownanie#

Wybor miedzy Flutterem a React Native to jedno z najczestszych pytan w swiecie mobile development. Oto kluczowe roznice:

| Aspekt | Flutter | React Native | |--------|---------|-------------| | Jezyk | Dart | JavaScript/TypeScript | | Renderowanie | Wlasny silnik (Skia/Impeller) | Natywne komponenty platformy | | Wydajnosc | Bardzo wysoka (kompilacja AOT) | Wysoka (JSI, Fabric) | | UI | Pixel-perfect, identyczny na kazdej platformie | Natywny look and feel platformy | | Ekosystem | Rosnacy, Google-backed | Bardzo duzy, Meta-backed | | Hot Reload | Tak (bardzo szybki) | Tak (Fast Refresh) | | Krzywa uczenia | Srednia (trzeba nauczyc sie Dart) | Niska (JavaScript/React) | | Web/Desktop | Oficjalne wsparcie | React Native Web (community) | | Rozmiar aplikacji | Wiekszy (wbudowany silnik renderujacy) | Mniejszy (natywne komponenty) |

Kiedy wybrac Flutter?#

  • Potrzebujesz identycznego UI na wszystkich platformach
  • Zalezy Ci na wysokiej wydajnosci animacji
  • Planujesz aplikacje na mobile, web i desktop
  • Chcesz szybki development dzieki Hot Reload
  • Tworzysz aplikacje z niestandardowym, bogatym UI

Kiedy wybrac React Native?#

  • Zespol ma doswiadczenie w JavaScript/React
  • Potrzebujesz natywnego look and feel platformy
  • Chcesz korzystac z duzego ekosystemu npm
  • Integrujesz sie z istniejaca aplikacja natywna
  • Rozmiar aplikacji jest krytyczny

Najlepsze praktyki Flutter#

  1. Uzywaj const constructors - oznaczaj widgety jako const gdzie to mozliwe, aby uniknac niepotrzebnych przebudowan
  2. Unikaj nadmiernego zagniezdenia - wyodrebniaj podwidgety do osobnych klas
  3. Stosuj odpowiedni state management - Provider dla prostych przypadkow, Riverpod lub Bloc dla zlozonych aplikacji
  4. Korzystaj z DevTools - Flutter DevTools oferuje zaawansowane narzedzia do profilowania, debugowania i inspekcji widget tree
  5. Implementuj responsywny design - uzywaj MediaQuery, LayoutBuilder i Flex widgets
  6. Optymalizuj listy - uzywaj ListView.builder zamiast ListView z dzieclmi dla dlugich list
  7. Piszcie testy - unit, widget i integration testy zapewniaja stabilnosc aplikacji

Podsumowanie#

Flutter i Dart to potezny duet do tworzenia nowoczesnych aplikacji cross-platform. Unikalna architektura Fluttera, bogaty ekosystem widgetow, wsparcie dla wielu platform i wyjatkowe doswiadczenie deweloperskie sprawiaja, ze jest to jeden z najlepszych wyborow do tworzenia aplikacji mobilnych, webowych i desktopowych w 2024 roku. Niezaleznie od tego, czy budujesz MVP, czy enterprise-grade aplikacje, Flutter dostarcza narzedzia i wydajnosc, ktorych potrzebujesz.


Potrzebujesz profesjonalnej aplikacji cross-platform? Zespol MDS Software Solutions Group specjalizuje sie w tworzeniu wydajnych aplikacji Flutter na iOS, Android, Web i Desktop. Skontaktuj sie z nami, aby omowic Twoj projekt i dowiedziec sie, jak mozemy pomoc Ci zrealizowac Twoja wizje.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Flutter i Dart - Cross-platform development w praktyce | MDS Software Solutions Group | MDS Software Solutions Group