Widgety we Flutterze – jak tworzyć bloki UI?

Widgety we Flutterze – jak tworzyć bloki UI?

Flutter, wieloplatformowy zestaw narzędzi UI, zyskał ogromną popularność dzięki szybkim cyklom rozwoju i imponującej wydajności. U podstaw jego sukcesu leży bogaty ekosystem widżetów, który pozwala z łatwością tworzyć interfejsy użytkownika.

W tym artykule dowiesz się więcej o widżetach we Flutterze – poznasz ich rodzaje, sposoby tworzenia i rolę w budowaniu złożonych interfejsów.

Czym są widżety we Flutterze?

To podstawowe elementy konstrukcyjne wykorzystywane do budowy interfejsu użytkownika. Można je traktować jako indywidualne komponenty, które opisują wizualną reprezentację i zachowanie poszczególnych części UI.

Każdy aspekt aplikacji – od prostych przycisków po złożone układy – składa się z różnych widżetów zorganizowanych w hierarchiczną strukturę znaną jako drzewo widżetów.

Przykład drzewa widżetów

Czemu warto korzystać z widżetów?

Flutter oferuje szeroką gamę widżetów o różnym przeznaczeniu. Modułowe podejście pozwala tworzyć elementy interfejsu, które mogą być wielokrotnie wykorzystywane w różnych widokach i w wielu aplikacjach. Ich wygląd oraz funkcje dostosowujesz później do własnych potrzeb.

Każdy widżet ma inną rolę. Niektóre są odpowiedzialne za umieszczanie elementów w określonej kolejności. Inne mają za zadanie wyświetlać obrazy z folderów systemowych.

Można śmiało powiedzieć, że widżety opisują dane, które chcesz zaprezentować użytkownikom lub od nich otrzymać.

Typy widżetów

Widżety we Flutterze odpowiadają różnym wymaganiom, które stawiamy w odniesieniu do UI.

Wyróżniamy dwa główne rodzaje widżetów:

  • StatelessWidgets – reprezentują widżety, które są niezmienne i samoistnie nie przechodzą w inny stan. Innymi słowy, są to bloki, które nie wymagają interakcji użytkownika ani dynamicznych aktualizacji.
  • StatefulWidgets – widżety, które mogą zmieniać swój stan, gdy są zbudowane w drzewie widgetów. Każdy element wejściowy lub wizualny, który opiera się na interakcji z użytkownikiem, będzie należał do tej kategorii.

Według oficjalnej segmentacji zamieszczonej w dokumentacji Fluttera, widżety są również dzielone na kategorie na podstawie ich zastosowania.

  1. Dostępność – dzięki widżetom z tej kategorii aplikacja może być dostępna dla osób niepełnosprawnych.
  2. Animacje i ruch – dzięki nim aplikacja staje się bardziej interaktywna.
  3. Zasoby, obrazy i ikony – widżety, które służą do zarządzania zasobami oraz wyświetlania obrazów i ikon.
  4. Asynchroniczność – takie widżety umożliwiają asynchroniczną interakcję z użytkownikami.
  5. Podstawy – zestaw niezbędnych widżetów, które każdy programista może wykorzystać do tworzenia widżetów bardziej złożonych.
  6. Cupertino – widżety zaprojektowane dla iOS.
  7. Input – kategoria z widżetami, które obsługują wszystkie dane wejściowe, które chcesz zebrać od użytkowników.
  8. Modele interakcji – używaj widżetów z tej kategorii, gdy chcesz wykrywać i obsługiwać interakcje użytkownika za pomocą zdarzeń dotykowych.
  9. Layout – widżety odpowiedzialne za rozmieszczanie innych widżetów w określony sposób (np. w wierszach lub kolumnach).
  10. Material Components – odpowiednik Cupertino dla Androida; te widżety są zgodne z zasadami Google Material Design.
  11. Malowanie i efekty – widżety, które stosują efekty wizualne do widżetów podrzędnych bez wpływu na ich układ, rozmiar lub położenie.
  12. Przewijanie – kategoria umożliwiająca użytkownikom przewijanie zestawu widżetów, których domyślnie nie można przewijać.
  13. Styl – kategoria przydatna, gdy chcesz zmienić motyw interfejsu użytkownika, zarządzać responsywnością aplikacji lub dostosować rozmiary widżetów.
  14. Tekst – kategoria z widżetami używanymi do wyświetlania i stylizowania tekstu.

Każda z tych kategorii zawiera wiele widżetów. Poniżej wypisałem te podstawowe, które zna większość programistów:

  • Text Widget,
  • Container Widget,
  • Image Widget,
  • Center Widget,
  • Column and Row Widget.

Jeśli zagnieździsz te bloki jako widżety podrzędne dla siebie nawzajem i użyjesz właściwości stylu, możesz stworzyć wizualną reprezentację swojej koncepcji.

Przyjrzyjmy się teraz niektórym aplikacjom eCommerce, aby dowiedzieć się, jak w praktyce działają flutterowe widgety.

Przykłady zastosowania widżetów w aplikacjach mCommerce

Poniżej przedstawiam różne układy i funkcje. Możesz je tworzyć na wiele sposobów. Kod, który ci udostępniam, jest uproszczony i przedstawia jedynie moje propozycje rozwiązań. Istnieją też inne podejścia, które również mogą się sprawdzić.

Ikea

W aplikacjach mCommerce ważne jest, aby wyświetlać produkty w sposób, który przyciąga wzrok. Dzięki temu klienci są bardziej zainteresowani ofertą. Ekran główny Ikea jest pełen inspiracji dotyczących wystroju wnętrz.

Źródło: aplikacja Ikea 

Oprócz skanera kodów kreskowych, który ułatwia zakupy offline w sklepach, użytkownicy widzą obrazy umieszczone na przejrzystej siatce.

Jak stworzyć taki widok za pomocą widżetów? Po pierwsze, wypisz jego elementy składowe – opisz, co powinno się wyświetlać na ekranie i jak użytkownicy mogą z tym wchodzić w interakcje.

Potem, na podstawie zdjęcia, wypisz oczekiwane rezultaty, zapisz filtry z tagami dla każdego pomieszczenia i dodaj siatkę zdjęć z inspiracjami. Użytkownicy powinni mieć możliwość przewijania sekcji z filtrami zarówno w poziomie, jak i w pionie. Tak więc w sekcji filtrów potrzebujesz sposobu na przewijanie treści pojedynczo na boki, podczas gdy pozostałe elementy muszą być przewijane w górę i w dół.

Implementacja

Zależało mi na tym, żeby implementacja była tak szczegółowa jak to możliwe, pominąłem więc tekst i filtry i przeszedłem od razu do podstawowego elementu widżetu. Jak widać, nie jest on symetryczny. Prowadzi to do problemów z GridView, ponieważ on właśnie buduje siatkę w symetryczny sposób. Dlatego sugeruję użycie siatki rozłożonej (staggered grid).

Tutaj znajdziesz pakiet, który się tym zajmie. Oferuje różne możliwości budowania ciekawie wyglądających siatek.

Kod jest dostępny na naszym koncie na Githubie.

MasonryGridView.builder(
       shrinkWrap: true,
       itemCount: 100,
       physics: const NeverScrollableScrollPhysics(),
       gridDelegate: const SliverSimpleGridDelegateWithFixedCrossAxisCount(
         crossAxisCount: 2,
       ),
       itemBuilder: (context, index) => Padding(
         padding: const EdgeInsets.all(2),
         child: Container(
           decoration: BoxDecoration(
             borderRadius: BorderRadius.circular(12),
             color: [
               Colors.blueGrey,
               Colors.greenAccent,
               Colors.redAccent,
               Colors.white54,
               Colors.lightBlueAccent,
             ][index % 5],
           ),
           height: [
             100.0,
             150.0,
             50.0,
             200.0,
             300.0,
           ][index % 5],
         ),
       ),
     )

Ze względu na fakt, że ten konstruktor oferuje funkcję przewijania po wyjęciu z pudełka, żeby ją wyłączyć musisz ustawić NeverScrollableScrollPhysics. Dzięki temu możesz używać SingleChildScrollView wraz z widżetem Column do budowy layoutu.

Jeśli zauważyłeś, że shrinkWrap jest ustawiony na true, nie martw się – to nie to samo co ListView. Zgodnie z dokumentacją “Ten konstruktor jest odpowiedni dla widoków siatki z dużą (lub nieskończoną) liczbą widżetów podrzędnych, ponieważ jest wywoływany tylko dla tych, które są rzeczywiście widoczne.” 

Rezultat powinien wyglądać tak:

Co jeśli lista jest nieskończona i wymaga paginacji? W takim przypadku sugerowałbym użycie Slivers, ponieważ daje on więcej opcji, jeśli chodzi o przewijane układy.

Homla

Oryginalna aplikacja Homla została napisana w językach natywnych – Kotlin i Swift. Niemniej jednak możemy spróbować sobie wyobrazić, jak by to było zbudować jedną z jej najfajniejszych funkcji za pomocą Fluttera.

Źródło: aplikacja Homla

Skupimy się na widoku produktu, ponieważ wykorzystuje on zagnieżdżone przewijanie w nietypowy sposób. Na potrzeby tego artykułu nazwę go split scrolling.

Zamysł jest prosty – na jednym widoku użytkownicy przewijają zawartość na dwa sposoby. W górnej sekcji przewijają zdjęcia produktów z góry na dół. Ale kiedy przechodzą do sekcji poniżej (ze szczegółami produktu), po dotknięciu rozszerza się ona na cały ekran.

Split scrolling we Flutterze

Najpierw przygotuj listę elementów, które powinny znaleźć się w każdej sekcji ekranu. Następnie zapisz, co zamierzasz zaimplementować w swoim demo.

Jak widać, na zdjęcie nakłada się w niewielka część sekcji szczegółów. Oprócz tego mamy przyciski z Call to action i wskaźnik, który pokazuje aktualnie wyświetlany obraz. W sekcji szczegółów nie ma nic nietypowego, więc w demo zastąpimy ją kolorowym panelem.

Podsumowując, dla tych dwóch elementów potrzebujemy przewijanego widżetu nadrzędnego, który może obsługiwać przewijalny widżet podrzędny.

Aby rozwiązać ten problem, musisz wrócić do Slivers, użyć CustomScrollView i zaimplementować przewijanie strony bazowej. Następnie przejdź do górnej części ekranu, z obrazami produktów.

Przewijanie w prawo i lewo jest często stosowane we Flutterowych aplikacjach. Najprostszym sposobem na implementację tego zachowania jest użycie PageView. Musisz pamiętać, że widżet nadrzędny przewijania akceptuje Slivery zamiast Widgetów, więc musisz użyć SliverToBoxAdapter.

Pozostałą wolną przestrzeń przeznacz na sekcję ze szczegółami produktu. W tym przypadku jest po prostu kolorowy panel. Aby go stworzyć, najlepiej użyć jednego z dwóch sliverów, które mamy we Flutterze. Wybrałem SliverFillRemaining, ale można to również łatwo zrobić za pomocą SliverToBoxAdapter.

Spójrz na uproszczony wynik i kod:

CustomScrollView(
         slivers: [
           SliverToBoxAdapter(
             child: Stack(
               alignment: Alignment.bottomCenter,
               children: [
                 const _ProductView(),
                 Container(
                   decoration: const BoxDecoration(
                     color: Colors.red,
                     borderRadius:
                         BorderRadius.vertical(top: Radius.circular(16.0)),
                   ),
                   height: 32.0,
                 )
               ],
             ),
           ),
           SliverFillRemaining(
             child: Container(
               color: Colors.red,
               height: MediaQuery.of(context).size.height / 2,
             ),
           ),
         ],
       ),


class _ProductView extends StatefulWidget {
 const _ProductView();


 @override
 State<_ProductView> createState() => _ProductViewState();
}


class _ProductViewState extends State<_ProductView> {
 final PageController _pageController = PageController(initialPage: 0);
 int _activeIndex = 0;


 List<Widget> get productPhotos => [
       Image.asset(
         'assets/shoes.jpg',
         fit: BoxFit.contain,
       ),
       Image.asset(
         'assets/shoes.jpg',
         fit: BoxFit.contain,
       ),
       Image.asset(
         'assets/shoes.jpg',
         fit: BoxFit.contain,
       ),
     ];


 @override
 Widget build(BuildContext context) => SizedBox(
       height: MediaQuery.of(context).size.height / 2,
       child: Stack(
         alignment: Alignment.centerLeft,
         children: [
           PageView.builder(
             controller: _pageController,
             onPageChanged: (page) => setState(() => _activeIndex = page),
             itemCount: productPhotos.length,
             itemBuilder: (context, index) => productPhotos[index],
             scrollDirection: Axis.vertical,
           ),
Column(
             mainAxisAlignment: MainAxisAlignment.center,
             children: List.generate(
               productPhotos.length,
               (index) => _dot(_activeIndex == index),
             ),
           ),
         ],
       ),
     );


 Widget _dot(bool isPicked) => Padding(
       padding: const EdgeInsets.all(8.0),
       child: Container(
         decoration: BoxDecoration(
           borderRadius: BorderRadius.circular(4.0),
           color: isPicked ? Colors.black87 : Colors.black38,
         ),
         height: 8.0,
         width: 8.0,
       ),
     );


 @override
 void dispose() {
   _pageController.dispose();
   super.dispose();
 }
}

CCC

Ta aplikacja przyciągnęła moją uwagę ze względu na dwie funkcje. Widziałem je wiele razy w aplikacjach webowych, ale dzięki CCC po raz pierwszy zobaczyłem, jak można je wykorzystać również w aplikacji mobilnej.

Karuzele

Pierwszą funkcją są nieskończone karuzele. Gdy użytkownicy przewijają ich zawartość, aplikacja wyświetla kolejne elementy. Na przykład baner z informacjami o rabatach, produktach itp.

Możesz dodać taką karuzelę do swojej aplikacji. Istnieją gotowe pakiety stworzone specjalnie do tego celu, więc nie trzeba kodować karuzeli samodzielnie. Jeśli jednak nie chcesz z nich korzystać, sugeruję stworzenie PageView obsługiwanego przez Timer. Automatycznie obsługuje on animację do następnej strony po ustalonym czasie. Koniec animacji wyzwala akcję, która dodaje element na koniec listy i usuwa go z pierwszej pozycji.

Wybrałem pakiet o nazwie carousel_slider. Oferuje wiele opcji, które zmieniają wygląd karuzeli. Jestem pewien, że sprawdzi się on w większości przypadków.

Źródło: aplikacja CCC

Ta implementacja jest naprawdę prosta. Wystarczy ustawić czas, po którym powinien pojawić się nowy element i procent ekranu, jaki musi on zajmować. Następnie należy dostarczyć widżety podrzędne dla karuzeli, aby zbudować listę.

CarouselSlider.builder(
           itemCount: 3,
           itemBuilder: (context, itemIndex, pageIndex) => Padding(
             padding: const EdgeInsets.only(left: 16.0),
             child: Container(
               color: [
                 Colors.red,
                 Colors.blue,
                 Colors.green,
               ][itemIndex % 3],
             ),
           ),
           options: CarouselOptions(
             autoPlay: true,
             viewportFraction: 0.7,
             disableCenter: true,
             padEnds: false,
             height: MediaQuery.of(context).size.height / 2,
           ),
         ),

Te kilka linijek kodu może wywołać efekt, który zachęci klienta do ponownego spojrzenia na coś, co chcesz mu pokazać.

Wynik powinien być podobny do tego:

Zaawansowany efekt przewijania: paralaksa

Przejdźmy teraz do najbardziej złożonego przykładu ze wszystkich. Efekt przewijania – paralaksa – znajduje się w sekcji ze szczegółami produktu.

Po raz kolejny użyjemy Sliverów, ale tym razem z animacją. Pozwoli nam to kontrolować elementy wyświetlane na ekranie w odniesieniu do postępu przewijania.

 

Najpierw przyjrzyj się funkcji, aby wiedzieć, co należy wdrożyć:

  1. Wszystkie elementy w górnej sekcji można przewijać w różnych kierunkach.
  2. Przyciski przesuwają się na boki.
  3. Obraz wydaje się przesuwać w innym tempie niż pozostałe elementy.

Ten rodzaj zachowania nazywany jest efektem paralaksy. Ma on zastosowanie jedynie w odniesieniu do wskaźnika i obrazu. Tylko te elementy przewijają się nad sobą na jednej osi, więc jest to jedyne miejsce, w którym ten efekt jest widoczny. W przypadku ikony będziesz polegać głównie na operatorze offset, który pozwoli ci ją przenieść na inną oś. Aby to zaimplementować, utwórz animację, która nadaje widżetom podrzędnym różne prędkości podczas przewijania.

Zacznij od wykorzystania przewijanego widżetu. Potrzebujesz również sposobu na kontrolowanie następujących elementów:

  • obrazu,
  • wskaźnika,
  • ikon.

Żeby kontrolować ich położenie, użyj widżetu Stack i Positioned. Następnie ustaw wartości kontrolujące widżety.

 final ScrollController _scrollController = ScrollController();
 final PageController _pageController = PageController();
 double get screenHeight => MediaQuery.of(context).size.height;
 double get scrollingOffset =>
     _scrollController.hasClients ? _scrollController.offset : 0.0;

Teraz musisz określić, w którym miejscu przewijania się znajdujesz. Użyj operatora przesunięcia (offset), a MediaQuery pobierze informacje o wysokości z urządzenia.

Aby kontynuować, potrzebujesz dwóch elementów. Przewijanego widżetu z miejscem na zdjęcia produktów oraz listy kafelków ze szczegółowymi informacjami. Na potrzeby tego przykładu uprościmy tę listę.

Widget _productDetails() => ListView(
       cacheExtent: 64,
       controller: _scrollController,
       children: [
         /// Space that we want our product in
         SizedBox(height: screenHeight * 0.35),
         ...List.generate(
           10,
           (index) => Container(
             height: 100,
             color: [
               Colors.red,
               Colors.blue,
               Colors.green,
             ][index % 3],
           ),
         )
       ],
     );

Dostarcz ten kod do Stacka wraz z widżetem, aby kontrolować zachowanie ikony.

Widget _actionIcon() => Positioned(
       top: screenHeight * 0.2 + kTextTabBarHeight - scrollingOffset,
       right:
           16 - scrollingOffset * 0.25,
       child: IconButton(
         onPressed: () => setState(() => _isFavorite = !_isFavorite),
         icon: Icon(
           _isFavorite ? Icons.favorite : Icons.favorite_border,
           color: _isFavorite ? Colors.red : Colors.black,
         ),
       ),
     );

 

Do ikony możesz dodać animację, żeby wysuwała się z ekranu. Wystarczy zmienić pozycję prawego wypełnienia na podstawie przesunięcia.

Teraz zrób to samo ze zdjęciem i wskaźnikiem, włączając przesunięcie przewijania (scrolling offset).

Widget _productPhotos() => Positioned(
       top: -0.6 * scrollingOffset - kToolbarHeight,
       right: 0,
       left: 0,
       height: screenHeight * 0.35,
       child: RepaintBoundary(
         child: PageView(
           onPageChanged: (page) =>
               setState(() => _currentProductPhoto = page),
           controller: _pageController,
           children: productPhotos,
         ),
       ),
     );


 Widget _productPhotoIndicator() => Positioned(
       top: screenHeight * 0.25 + kTextTabBarHeight - scrollingOffset,
       left: 0,
       right: 0,
       child: Row(
         mainAxisAlignment: MainAxisAlignment.center,
         children: List.generate(
           productPhotos.length,
(index) => _indicatorDot(index),
         ),
       ),
     );


 Widget _indicatorDot(int index) => Padding(
       padding: const EdgeInsets.symmetric(horizontal: 8),
       child: InkWell(
         onTap: () => _pageController.animateToPage(
           index,
           duration: const Duration(milliseconds: 300),
           curve: Curves.easeIn,
         ),
         child: CircleAvatar(
           radius: 8,
           backgroundColor:
               index == _currentProductPhoto ? Colors.black : Colors.black12,
         ),
       ),
     );

Po dodaniu wszystkiego do Stacka powinien pojawić się następujący rezultat:

 

Przewijanie zachowuje się inaczej na każdym z elementów. Dzięki tej prostej manipulacji stworzyłeś zgrabny efekt, który może poprawić wrażenia użytkownika.

Podsumowując, widżety stanowią podstawę rozwoju aplikacji tworzonych we Flutterze. Szeroki wybór dostępnych widżetów pozwala budować złożone interfejsy i personalizować je. Baza ta nieustannie rośnie dzięki społeczności, która tworzy pakiety, zawierające ciągle nowe opcje do wyboru.

Gdy znasz różne typy widżetów i wiesz, jak je tworzyć lub łączyć ze sobą, możesz w pełni odkrywać potencjał Fluttera. Zachęcam więc do zanurzenia się w świecie widżetów i uwolnienia swojej kreatywności!

Marcel - Flutter Developer

Marcel Kozień

Flutter Developer, którego pasjonuje tworzenie wydajnego oprogramowania. Wierzy, że w przypadku rozwoju aplikacji mniej znaczy więcej. Kiedy nie pisze kodu, ogląda dobre filmy, buduje konstrukcje z Lego do swojej coraz bogatszej kolekcji i odkrywa nowe miejsca dzięki podróżom.

Dowiedz się więcej

Wycena projektu

Opowiedz nam o swoim projekcie i napisz, jak możemy Ci pomóc.

Dlaczego warto rozwijać z nami projekty?

Logo Mobile Trends Awards

Mobile Trends Awards 2021

Wygrana w kategorii
ŻYCIE CODZIENNE

Nagroda Legalnych Bukmacherów

Nagroda Legalnych Bukmacherów 2019

Najlepsza aplikacja mobilna

Mobile Trends Awards logo

Mobile Trends Awards 2023

Wygrana w kategorii
MCOMMERCE ROZWÓJ

23

opinie klientów

Clutch logo