Mobile

Unifikacja języka UI w aplikacjach mobilnych na przykładzie Jetpack Compose & Flutter

flutter compose

Częścią pracy każdego programisty jest wybór technologii, w których chcielibyśmy się rozwijać. Technologie mobilne, jak i same urządzenia – telefony i tablety, mimo upływu dekady od powstania pierwszego iPhone’a, ciągle rozwijają się w szalonym wręcz tempie. To samo dotyczy języków, których używa się do pisania na nie oprogramowania. Czym powinniśmy kierować się przy wyborze ścieżki naszej kariery jako programiści mobilni? Statystykami, wieszczeniem wróżki Vanessy czy własnym przeczuciem?

Skłaniałbym się ku rzeczowej analizie rynkowych trendów. Jednym z takich trendów jest zmiana języków definicji UI, rozpoczęta wśród programistów iOS, kontynuowana w React Native i Fluterze i dopełniona w Androidzie pod postacią Jetpack Compose.

Źródło grafiki

Początki powrotu do code-first approach

W czasach objective-C interfejsy aplikacji iOS definiowaliśmy w plikach *.xib/*.storyboard pod postacią długich, małoczytelnych dokumentów xml. Sytuacja wyglądała tutaj dużo gorzej niż na konkurencyjnym Androidzie, który mimo użycia równie rozwlekłego formatu, kontrolki zapisywał w logicznym porządku, gdzie id nie były generowane losowo, a nazywane przez programistę. W dłuższej perspektywie okazało się to zbawienne dla utrzymania spójności i unikania konfliktów podczas mergowania kodu inżynierów pracujących nad tym samym modułem.

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LoginView" customModule="Xib_Demo" customModuleProvider="target">
            <connections>
                <outlet property="accountMessageLabel" destination="RHZ-BM-iU1" id="Egt-mr-kFX"/>
                <outlet property="contentView" destination="iN0-l3-epB" id="m3I-eR-AZ3"/>
                <outlet property="emailTextField" destination="N2q-2h-lNG" id="C6A-5b-j3O"/>
                <outlet property="loginButton" destination="jtN-gs-dts" id="WQ1-T6-GRL"/>
                <outlet property="passwordTextField" destination="coH-nt-ZpH" id="Np3-GJ-RDk"/>
            </connections>
        </placeholder>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB">
            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="N2q-2h-lNG">
                    <rect key="frame" x="16" y="192.5" width="343" height="50"/>
                    <color key="backgroundColor" red="0.35367466469999997" green="0.71446218770000003" blue="1" alpha="1" colorSpace="calibratedRGB"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="50" id="KbO-t6-m5M"/>
                    </constraints>
                    <nil key="textColor"/>
                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
                    <textInputTraits key="textInputTraits"/>
                </textField>
                <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="coH-nt-ZpH">
                    <rect key="frame" x="16" y="250.5" width="343" height="50"/>
                    <color key="backgroundColor" red="0.35367466469999997" green="0.71446218770000003" blue="1" alpha="1" colorSpace="calibratedRGB"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="50" id="1es-ji-Ika"/>
                    </constraints>
                    <nil key="textColor"/>
                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
                    <textInputTraits key="textInputTraits"/>
                </textField>
                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jtN-gs-dts">
                    <rect key="frame" x="16" y="308.5" width="343" height="50"/>
                    <color key="backgroundColor" red="1" green="0.33229502220000001" blue="0.06197259519" alpha="1" colorSpace="calibratedRGB"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="50" id="bN2-Y6-1gu"/>
                    </constraints>
                    <fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
                    <state key="normal" title="Button">
                        <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    </state>
                </button>
                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="New User? Create an account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RHZ-BM-iU1">
                    <rect key="frame" x="73.5" y="366.5" width="228" height="21"/>
                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
                    <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <nil key="highlightedColor"/>
                </label>
            </subviews>
            <color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            <constraints>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="N2q-2h-lNG" secondAttribute="trailing" constant="16" id="0kh-Dm-mef"/>
                <constraint firstItem="jtN-gs-dts" firstAttribute="top" secondItem="coH-nt-ZpH" secondAttribute="bottom" constant="8" id="6kK-A1-7eu"/>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="coH-nt-ZpH" secondAttribute="trailing" constant="16" id="9sW-o7-1MF"/>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="jtN-gs-dts" secondAttribute="trailing" constant="16" id="ECy-bW-nIk"/>
                <constraint firstItem="RHZ-BM-iU1" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="Hmj-v7-nfh"/>
                <constraint firstItem="jtN-gs-dts" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="RiZ-B1-Mmg"/>
                <constraint firstItem="coH-nt-ZpH" firstAttribute="top" secondItem="N2q-2h-lNG" secondAttribute="bottom" constant="8" id="Wfv-1H-3yP"/>
                <constraint firstItem="jtN-gs-dts" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="awJ-Cd-r2w"/>
                <constraint firstItem="RHZ-BM-iU1" firstAttribute="top" secondItem="jtN-gs-dts" secondAttribute="bottom" constant="8" id="c34-gr-HI8"/>
                <constraint firstItem="jtN-gs-dts" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="f7f-ew-nTT"/>
                <constraint firstItem="coH-nt-ZpH" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="jgR-1Z-ydA"/>
                <constraint firstItem="N2q-2h-lNG" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="s9y-nv-M4J"/>
            </constraints>
            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
            <point key="canvasLocation" x="138.40000000000001" y="154.27286356821591"/>
        </view>
    </objects>
</document>

Źródło kodu

Programiści iOS skłonili się więc ku SwiftUI, który oferował ucieczkę od problematycznego formatu *.xib, a jednocześnie pozwalał na:

  1. definicję UI bezpośrednio w kodzie – tym samym na nawigację do kodu kontrolek i łatwego czytania dokumentacji, która się tam znajduje,
  2. łatwe użycie funkcji zdefiniowanych w innych miejscach kodu (np. do formatowania, czy walidacji), a nawet zawarcie tam prostej logiki (UWAGA: mieszanie kodu UI i logiki biznesowej jest anty-patternem i nigdy tego nie rób jeżeli nie masz konkretnego powodu).

Oczywiście definicja UI z poziomu kodu ma też swoje wady, o czym później.

kod źródłowy apple swift

Źródło grafiki

Ciekawym aspektem jest fakt, że SwiftUI, czy szerzej definiowanie interfejsu użytkownika z poziomu kodu aplikacji jest tak naprawdę powrotem do korzeni informatyki. Dużym osiągnięciem było rozdzielenie zapisu kształtu UI do osobnych plików – najpierw poprzez pliki *.xml, a następnie poprzez wprowadzenia bindingów i dwustronnych data-bindingów. Ułatwiało to początkującym programistom utrzymanie porządku w architekturze aplikacji i klarowne rozdzielenie logiki biznesowej – zarówno w architekturze MVP, jak i MVVM.

Flutter – mocny gracz wkracza do akcji już na starcie rozgrywając React-Native

Flutter odbił się niemałym echem w świecie mobilnego multiplatform. Jest on zupełnie nowym podejściem – między innymi po Xamarinie, którego największą bolączką było wrapowanie androidowych kontrolek, zmuszające twórców do pogoni za każdą kolejną wersją platformy i z góry skazanym na porażkę. Co ciekawe Flutter już przy swoim starcie rozwiązał także bolączkę React-native – niewydajnego drzewa DOM, nawigacji na Androidzie i opóźnień w odświeżeniu UI przy dużym obciążeniu. Interfejs zbudowany na silniku Chrome – Blink – pozwala na stałe utrzymywanie 60 klatek/s, aplikacje nie dławią się, a my mamy dostęp do całej palety przepięknych animacji.

Omawiany framework, podejście code-first w UI ma wbudowane od samego startu, a interfejsy definiowane w Darcie, wyglądają następująco:

// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Źródło kodu

Musicie przyznać, że mamy tu analogię do wspomnianego wcześniej SwiftUI.

Ostatni gracz na rynku – Jetpack compose

Jetpack compose, od niedawna w wersji stabilnej, pojawił się jako ostatni. Zamyka on cykl przejścia do nowego sposobu definiowania UI z poziomu kodu we wszystkich wiodących platformach mobilnych i jasno pokazuje czego możemy spodziewać się w najbliższy dwóch latach.

Kotlin jest zdecydowanie najpotężniejszym z wymienionych tutaj języków – w związku z czym sam Jetpack Compose ma także najwięcej możliwości – nie tylko rozszerzania, ale swobodnej ingerencji w zachowanie UI.

W przeciwności do SwiftUI – Jetpack Compose pretenduje też do bycia multiplatformowym frameworkiem definiowania interfejsów… a co z tego wyjdzie – zobaczymy. Statystyki użycia jasno pokazują, że na razie rynkowo nie udało mu się wyjść poza świat Androida.

comsable

Źródło kodu

Jetpack-compose vs. Flutter

Spójrzmy na prosty ekran logowania napisany w Jetpack Compose i Flutter. Od razu rzucają się nam w oczy ewidentne podobieństwa:

  • w obu rozwiązaniach definiujemy widgety, które odświeżają stan,
  • w obu rozwiązaniach mamy wiersze/kolumny zamiast typowych LinearLayout, czy ConstraintLayout,
  • w obu rozwiązaniach możemy stosować flex znany ze świata webowego,
  • oczywiście oba rozwiązania korzystają z natywnego języka Kotlin/Dart, a więc możemy korzystać z funkcji/zaawansowanych rozwiązań bezpośrednio w kodzie UI.

Jetpack Compose:

Źródło kodu

Flutter:

Fluttera i Jetpack Compose pisały zupełnie inne zespoły programistów – widać jednak próby unifikacji podejść. Kod UI nie różni się znacząco od SwiftUI. Jak na razie nie jest możliwe bezpośrednie kopiowanie fragmentów UI pomiędzy Flutterem, a Jetpack Compose, ale może w przyszłości będzie?

flutter jetpack

Źródło kodu

Pomyślmy jak korzystna z punktu widzenia biznesu byłaba możliwość swobodnego wyboru pomiędzy Jetpack Compose, a Flutterem, czy więcej – zamiana implementacji UI według aktualnych potrzeb – skupienia się na funkcjonalności natywnej [Kotlin] lub wybór oszczędności wynikającej z cross-platformowego podejścia [Flutter].

Co więcej, prototyp UI napisany we Flutterze można by swobodnie wykorzystać do natywnej aplikacji kotlinowej z Composem i na odwrót.

Osadzanie modułów flutterowych wewnątrz aplikacji natywnych również stałoby się łatwiejsze.

Czy ja już tego gdzieś nie widziałem?

Dla starych wyjadaczy informatyki, definiowanie UI z poziomu kodu nie jest niczym nowym. W zasadzie to powrót do lat 2000, czy nawet początku 90-tych. Dlaczego wtedy od niego odchodziliśmy, a teraz, po 20 latach nagle wracamy?

Nadmienię tylko, że mieszanie definicji UI z kodem biznesowym jest niebezpieczną praktyką, a prowadzenie projektu z Jetpack Compose wymaga od programistów dużo samodyscypliny, jak również wnikliwych PR review – w przeciwnym razie nasza aplikacja skończy na śmietniku historii jako spaghetti.

spaghetti code

Źródło grafiki

Podsumowanie

W tym artykule przyjrzeliśmy się obecnie panującym trendom w dziedzinie definicji UI w świecie mobilnym. Przechodząc przez 3 platformy – Android, iOS, Flutter – widzimy na wszystkich z nich tę samą tendencję odejścia od xmli i przejścia ku definicji UI z poziomu kodu. Mamy więc pewność, że trend ten utrzyma się w najbliższych miesiącach, czy nawet latach.

Ciekawym smaczkiem jest fakt, że całkiem możliwa jest jeszcze większa unifikacja definicji UI i wisząca w powietrzu nadzieja na łatwe współdzielenie kodu UI pomiędzy platformami. Odpowiadając na pytanie zawarte w pierwszym akapicie: tak, jak najbardziej powinniście uczyć się SwiftUI, Jetpack Compose i Fluttera, ponieważ zostaną one z nami na dłużej.

teleturniej programista100k

Zdjęcie główne artykułu pochodzi z unsplash.com.

Przygodę z aplikacjami mobilnymi zaczął ponad 9 lat temu, a z samym programowaniem przyjaźni się już ponad dekadę. W czasie wolnym gra w squasha, chodzi po górach - przeplatając ten czas hobby związanymi z dronami i samochodami elektrycznymi. Zawodowo prowadzi przeróżne inicjatywy związane ze światem IT - od grupy flutterowej, do Patronażu dla stażystów.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://justjoin.it/blog/unifikacji-jezyka-ui-w-aplikacjach-mobilnych-na-przykladzie-jetpack-compose-flutter" order_type="social" width="100%" count_of_comments="8" ]