Tworzenie gier mobilnych w Flutter - struktura projektu i zasada działania

Utworzony przez Łukasz Sujkowski, dnia 22.11.2022
 585    0
Tworzenie gier mobilnych w Flutter - struktura projektu i zasada działania
W poprzednim artykule przedstawiłem proces instalacji i konfiguracji środowiska. Kolejnym krokiem w drodze do tworzenia gier mobilnych będzie przeanalizowanie struktury projektu i zapoznanie się z ideologią towarzyszącą Flutterowi. Tak jak w przypadku poprzedniego artykułu, będę opisywał podstawy, więc jeśli jesteś osobą która ma już jakieś doświadczenie, możesz przejść do kolejnego artykułu tej serii.

Struktura projektu Flutter

Spoglądając na strukturę projektu widzimy, że nie jest ona za bardzo skomplikowana. Analizując od góry, widzimy katalogi poprzedzone kropką, są to katalogi konfiguracyjne środowiska i nie będziemy sobie nimi zaprzątać głowy. Dalej mamy katalog android, zawiera on nic innego jak pliki potrzebne do kompilowania aplikacji. Przy okazji należy wyróżnić dwa pliki znajdujące się w tym katalogu. Pierwszy z nich to android/app/build.gradle zawierający konfigurację skryptu kompilującego projekt na platformę Android. Drugi to android/app/src/main/AndroidManifest.xml jest to główny plik projektu aplikacji dla systemu Android, w tym pliku między innymi ustalamy uprawnienia jakich będzie wymagała aplikacja. Kolejny folder ios zawiera pliki związane z kompilacją aplikacji na system iOS. Niestety aby skompilować i wydać aplikację na system iOS potrzeba komputera z MacOS, dlatego na razie nie będziemy się nim przejmować. Niemniej jednak nasz projekt jest przygotowany i w przyszłości po drobnej konfiguracji będziemy mogli wydać aplikację również na system iOS.  Następnym katalogiem jest lib tutaj znajduje się kod naszej aplikacji, obecnie mamy tam plik main.dart który zawiera kod wygenerowany przy tworzeniu projektu. Folder test zawiera pliki testów automatycznych, raczej nie będziemy tego używać. W katalogu głównym mamy jeszcze kilka pojedynczych plików, nie będziemy ich wszystkich opisywać, zajmiemy się tylko plikiem pubspec.yaml.

Plik konfiguracyjny pubspec.yaml projektu Flutter

Jest to plik konfiguracyjny naszego projektu. W nim definiujemy nazwę, wersję, opis aplikacji, minimalną wersję Fluttera na którym projekt może zostać uruchomiony (environment:sdk:), oraz biblioteki których wymaga projekt (dependencies). Należy zwrócić uwagę na fakt, że tabulatory mają w tym pliku znaczenie (nie korzystamy z nawiasów klamrowych). Jeżeli chodzi o pakiety, możemy instalować je na dwa sposoby, pierwszy to dopisanie do listy analogicznie jak pakiet cupertino_icons (po dwukropku wskazujemy wersję). Po takiej operacji w terminalu projektu należy wykonać komendę flutter pub get. Druga opcja to wykonanie komendy instalacyjnej np. flutter pub add in_app_purchase dla pakietu mikropłatności, również w terminalu projektu. Dostępne pakiety mozemy przeglądać na stronie https://pub.dev/, polecam zerknąć w wolnej chwili.

Myślę, że jeśli chodzi o strukturę to powinno na ten moment wystarczyć. W dalszej części artykułu, zamiast tłumaczyć domyślnie wygenerowany kod, napiszemy prostą aplikację od zera, ale najpierw kilka słów o widgetach o które opiera się ta technologia. 

Widget jest podstawowym elementem Fluttera, który służy do budowy aplikacji. Mogą być stanowe lub bezstanowe, te pierwsze mogą zmieniać się w skutek interakcji użytkownika. Jeśli chodzi o te bezstanowe, to między innymi tekst, obrazek, ikona, a z tych mniej prymitywnych na przykład szablon karty produktu na liście w sklepie internetowym. Widgety mogą się w sobie zagnieżdżać budując strukturę drzewiastą. Na przykład jeśli chcemy stworzyć komunikat o błędzie, korzystamy z widgetu kontenera (Container), nadajemy mu kolor tła, w środku zagnieżdżamy kolumnę (Column), aby móc ustawić elementy jeden po drugim, w niej dodajemy tekst (Text) zawierający treść, oraz (Button) zawierający tekst (Text) na przykład o treści "OK". Widget przycisku oferuje parametr onPressed w którym możemy reagować na kliknięcie. Nie wydaje się to trudne, prawda? Z tą wiedza możemy przejść do pisanie kodu. 

Na początek otwieramy plik lib/main.dart, a jego zawartość zastępujemy poniższym kodem.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}
Pierwsza linijka importuje pakiet potrzebny do tworzenia aplikacji. Poniżej widzimy funkcję główną main wywoływaną przy uruchomieniu aplikacji, w której znajduje się wywołanie funkcji runApp, która w jako parametr przyjmuje główny widget aplikacji - tutaj zaczyna się nasze drzewo widgetów. 

Kolejnym krokiem będzie utworzenie pierwszego widgetu o nazwie MyApp, w tym celu klikamy prawym przyciskiem myszy na katalog lib wybieramy New, a następnie Dart file, w nazwę wpisujemy ​​my_app rozszerzenie zostanie dodane automatycznie.

Analogicznie jak w przypadku main.dart importujemy pakiet material, a poniżej wprowadzamy fazę “st”, Android Studio powinien zaproponować skorzystanie z makra stless tworzącego widget bezstanowy. 

Tworzenie bezstanowego widgetu Flutter

Po wybraniu makra, kursor automatycznie ustawi się w miejsce w którym wprowadzamy nazwę MyApp, finalnie powinno wyglądać to tak:

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
Na razie skupmy się na metodzie build, która jest odpowiedzialna za budowanie UI naszego widgetu, w tym momencie zwraca ona pusty kontener, zamieńmy go na widget MaterialApp, i dodajmy jakiś tekst:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    child: Text(
      'Testowy tekst',
      style: TextStyle(
        color: Colors.green,
        fontSize: 42.0,
      ),
    ),
  );
}
Jak pewnie zauważyliście, dodany kod został podkreślony na żółto, chodzi o to, że dodana struktura widgetów nie ma w sobie nic dynamicznego (np. tekstu pobieranego ze zmiennej), w takim wypadku możemy poprzedzić go słowem kluczowym const (constant) co wpłynie pozytywnie wydajność. Należy pamiętać, że jeśli po tej zmianie dodamy do widgetu coś dynamicznego np. skorzystamy ze zmiennej w tekście, wyskoczy błąd, w takim wypadku należy usunąć słowo const. Po zmianach metoda build będzie wyglądała następująco:

@override
Widget build(BuildContext context) {
  return const MaterialApp(
    child: Text(
      'Testowy tekst',
      style: TextStyle(
        color: Colors.green,
        fontSize: 42.0,
      ),
    ),
  );
}
Pora na pierwsze uruchomienie aplikacji, jednak przed tym musimy poprawić błąd w pliku main.dart, który jest spowodowany brakiem importu widgetu MyApp, przy okazji poprzedzimy go słowem kluczowym const jako, że nie przyjmuje on żadnych zmiennych:

import 'package:flutter/material.dart';
import 'my_app.dart';

void main() {
  runApp(const MyApp());
}
Po szybkiej poprawce możemy uruchomić aplikację, wygląda ona mniej więcej tak:

Widok wykompilowanego projektu Flutter

Baza gotowa, teraz możemy doprowadzić ten widok do takiego stanu aby przypominał aplikację. Klikamy prawym przyciskiem myszy na komponent Text, z menu podręcznego wybieramy Show Context Actions, a następnie Wrap with widget….

Akcje kontekstowe we Flutterze i Android Studio

Po tej akcji, kursor sam ustawi się w miejscu wprowadzania nazwy widgetu, wpisujemy Scaffold i wciskamy enter, kursor przeskoczy na parametr child, nadpisujemy go parametrem body i wciskamy enter. Po tych operacjach metoda powinna wyglądać tak:

@override
Widget build(BuildContext context) {
  return const MaterialApp(
    home: Scaffold(
      body: Text(
        'Testowy tekst',
        style: TextStyle(
          color: Colors.green,
          fontSize: 42.0,
        ),
      ),
    ),
  );
}
Po szybkim uruchomieniu aplikacji zauważymy, że wygląd się zmienił. Myślę, że przyszła pora na kilka słów o wprowadzonych widgetach. Pierwszy z nich MaterialApp jest jednym z głównych widgetów Fluttera, wprowadza podstawową funkcjonalność aplikacji oraz Google Material Design czyli zbiór predefiniowanych komponentów do tworzenia interfejsu użytkownika. Jeśli chodzi o Scaffold odpowiada on za układ komponentów w aplikacji i to by było na tyle, reszta wątpliwości wyjaśni się przy okazji kolejnych zmian w naszej aplikacji. 

Wróćmy do kodu, aby dodać tytuł aplikacji w komponencie MaterialApp dodajmy parametr title: ‘Testowa aplikacja’, przy okazji dodajmy też belkę, aby tego dokonać w komponencie Scaffold dodajemy appBar: AppBar(child: const Text(‘Testowa Aplikacja’)), spowoduje to pojawienie się paska z tekstem na górze aplikacji. Jak pewnie zauważyliście, środowisko wyrzuca błąd, powód jest prosty: “const” przy komponencie MaterialApp. Komponent AppBar nie może być dodany do  komponentu oznaczonego jako stały, z tego powodu przenieśmy słowo kluczowe przed komponent Text w parametrze body jako, że on dalej pozostaje stały: 

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: "Testowa aplikacja",
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Testowa aplikacja'),
      ),
      body: const Text(
        'Testowy tekst',
        style: TextStyle(
          color: Colors.green,
          fontSize: 42.0,
        ),
      ),
    ),
  );
}
Po tych zmianach, nasza aplikacja zaczyna wreszcie wyglądać. Nie podoba mi się jednak niebieski kolor belki, zmieńmy go na zielony korzystając z komponentu ThemeData:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: "Testowa aplikacja",
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Testowa aplikacja'),
      ),
      body: const Text(
        'Testowy tekst',
        style: TextStyle(
          color: Colors.green,
          fontSize: 42.0,
        ),
      ),
    ),
  );
}
Wygląda dobrze, dodajmy jeszcze pływający przycisk z ikoną w prawym dolnym rogu. Użyjemy do tego parametru floatingActionButton, przyjmującego widget o takiej samej nazwie w komponencie układu Scaffold:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: "Testowa aplikacja",
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Testowa aplikacja'),
      ),
      body: const Text(
        'Testowy tekst',
        style: TextStyle(
          color: Colors.green,
          fontSize: 42.0,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {

        },
        tooltip: 'Zwiększ licznik',
        child: const Icon(Icons.add),
      ),
    ),
  );
}
Jak widać, w widgecie FloatingActionButton zagnieżdżona została ikona zamiast tekstu, treść dymka wyświetlającego się po przytrzymaniu przycisku został ustawiony na Zwiększ licznik. Dodatkowo została obsłużona akcja onPressed, na ten moment pustą funkcją. Logiką zajmiemy się po skończeniu zmian w samym wyglądzie interfejsu. Potrzebujemy jeszcze jednej rzeczy, a mianowicie kolejnej linii tekstu. Aby dodać tekst poniżej tego obecnie dodanego, użyjemy Column. Klikamy prawy na komponent Text, wybieramy Show Context Actions, a następnie Wrap with Column. Następnie w tablicy children dodajemy kolejny tekst o treści Stan licznika: 0. Przy okazji zmieńmy rozmiar tekstu:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: "Testowa aplikacja",
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Testowa aplikacja'),
      ),
      body: Column(
        children: [
          const Text(
            'Testowy tekst',
            style: TextStyle(
              color: Colors.green,
              fontSize: 16.0,
            ),
          ),
          Text(
            'Stan licznika: 0',
            style: TextStyle(
              color: Colors.green,
              fontSize: 16.0,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {

        },
        tooltip: 'Zwiększ licznik',
        child: const Icon(Icons.add),
      ),
    ),
  );
}
Ostatnim użytym przez nas widgetem będzie Padding, odpowiada on za odstępy wewnętrzne. Kliknijmy prawym przyciskiem myszy na widget Column, dalej Show Context Actions i Wrap with Padding, a następnie powtórzmy tą operacje dla pierwszego widgetu Text  w kolumnie. W przypadku tekstu, parametr padding ustawimy na EdgeInsets.only(bottom: 4.0), czyli odstęp będzie tylko od dołu. Jako “const” oznaczamy cały widget Padding z tekstem w środku:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: "Testowa aplikacja",
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Testowa aplikacja'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 4.0),
              child: Text(
                'Testowy tekst',
                style: TextStyle(
                  color: Colors.green,
                  fontSize: 16.0,
                ),
              ),
            ),
            Text(
              'Stan licznika: 0',
              style: TextStyle(
                color: Colors.green,
                fontSize: 16.0,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {

        },
        tooltip: 'Zwiększ licznik',
        child: const Icon(Icons.add),
      ),
    ),
  );
}
Środowisko prawdopodobnie będzie wyświetlało jeszcze ostrzeżenia dotyczące słowa kluczowego const, na razie się tym nie przejmujmy. Teraz pora na logikę, aby nasz widget MyApp mógł obsługiwać stany musimy go przekonwertować. W tym celu klikamy prawym przyciskiem myszy na nazwę widgetu i dalej z menu Show Context Actions wybieramy Convert to StatefulWidget. Po tej operacji, metoda build zostanie przeniesiona do nowo powstałej klasy reprezentującej stan aplikacji _MyAppState

Akcja konwersji na widget stanowy we Flutterze i Android Studio

Teraz możemy działać z logika. Dodajemy zmienną int _counter = 0; do klasy stanu powyżej metody build:

Utworzenie zmiennej widgetu stanowego

Wypisujemy zawartość zmiennej w drugim widgecie Text:

Wyświetlenie zawartości zmiennej

Metoda .toString() zamienia typ liczbowy (integer) na tekst. Jak widać na powyższym screenie, środowisko upomina o brak słowa const przed TextStyle, oczywiście można dodać. W ostatnim kroku odpowiednio uzupełniamy akcję onPressed naszego przycisku:

Obsługa akcji wciśnięcia przycisku

Tutaj zwracam uwagę na funkcję setState() jest ona odpowiedzialna za ustawienie stanu aplikacji, a tym samym odświeżenie widoku zawartego w metodzie build, co spowoduje aktualizację tekstu wyświetlającego zawartość zmiennej _counter. Po uruchomieniu aplikacji wciśnięcie przycisku powinno zwiększać licznik. Jakby pojawiły się jakieś problemy, poniżej załączam pliki main.dart oraz my_app.dart.

Końcowy efekt aplikacji Flutter

Tym sposobem ukończyliśmy nasza pierwszą aplikację Flutterową. W kolejnym artykule przeanalizujemy narzędzia do tworzenia gier, dostarczone wraz z ostatnią aktualizacją Fluttera.

Załączniki:

Komentarze

This site is protected by ReCaptcha and the Google Privacy Policy and Terms of Service apply.

Brak komentarzy...

Kategorie

Quixel  0
Blender  3
Unity  0
Flutter  5
Ogólne  3