Flutter VS React analogie tra i due frameworks

Alessandro Oddo

Flutter VS React analogie tra i due frameworks

Da programmatore Flutter ho avuto la possibilità in questo periodo di studiare React e sono rimasto colpito delle analogie tra i due linguaggi, il passaggio dall'uno all'altro è rapido comparato ad altri frameworks (AngularVue).

La principale analogia che voglio analizzare in questo articolo riguarda la definizione dei componenti statici e dinamici. Questo articolo non vuole andare a comparare prestazioni o definire quale sia il migliore tra i due framework, su questo penso che online possiate trovare articoli più specifici e secondo il mio parere personale dipende dai casi d'uso oltre che dalla conoscenza del framework stesso. Penso che questo confronto possa risultare utile per chi deve lavorare su uno di questi due frameworks non conoscendo l'altro.
vs

Tic-tac-toe


Partendo dal tutorial presente sul sito di React riscriverò qui l'equivalente Flutter dello stesso applicativo.

Il tutorial 
Reactcostruisce il classico gioco tic-tac-toe (comunemente detto "tris"), permette la gestione di una partita e mostra una history delle mosse effettuate. Il risultato finale si può vedere al seguentelink. Il risultato di quanto vedremo su 
Flutter è invece scaricabile da questo repository.

I componenti utilizzati

Il tutorial mette in gioco tre componenti:

  • Game -> è il componente principale che tiene lo stato del sistema e lo passa ai componenti sottostanti
  • Board -> si occupa di renderizzare i singoli quadrati che compongono la tavola di gioco.
  • Square -> renderizza il singolo quadrato.

Analizzeremo il codice presente per renderizzare ognuno di questi componenti analizzandone le differenze.

Notare che quelli che React chiama componenti non sono altro che widget per Flutter

Square

Il singolo quadrato di una casella del gioco viene renderizzato su React con questo codice: 

 
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}

.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}

L'equivalente su Flutter è questo:

 
class SquareWidget extends StatelessWidget {
final String value;
final Function() onClick;

const SquareWidget(this.value,this.onClick, {Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onClick,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
border: Border.all(width: 0),
),
child: Center(child: Text(value)),
),
);
}
}
 

Da qui si nota che in entrambi i casi andiamo a costruire un componente stateless.

Nel caso di React viene definita una funzione che torna tramite tag l'elemento da renderizzare con uno stile molto simile all'html (il formato si chiama JSX).

Nel caso di Flutter il componente viene descritto da una classe di tipo StatelessWidget il cui contenuto da renderizzare è descritto nella funzione build.

I parametri in ingresso in React sono passati attraverso la variabile props presente nella signature del metodo mentre in Flutter sono dati in ingresso al costruttore.

La differenza principale tra questi due framework sta nella gestione della vista. In React lo stile è descritto da un file css mentre in Flutter è definito dai widget usati (solitamente quelli di default sono definiti dalla libreria flutter/material.dart che sfrutta elementi di Material Design).

Se vi è la necessità di riutilizzare lo stile di un layout la soluzione proposta da Flutter è quella di suddividere il widget in sotto widget come nell'esempio sottostante:

 
class SquareWidget extends StatelessWidget {
final String value;
final Function() onClick;

const SquareWidget(this.value, this.onClick, {Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onClick,
child: SquareStyleWidget(
child: Text(value),
),
);
}
}

class SquareStyleWidget extends StatelessWidget {
final Widget child;

const SquareStyleWidget({required this.child, Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
width: 34,
height: 34,
decoration: BoxDecoration(
border: Border.all(width: 0),
),
child: Center(child: child),
);
}
}

Board

Ora descriviamo il componente Board. Per la parte di React abbiamo:

 
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}

render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
-----css-----

.board-row:after {
clear: both;
content: "";
display: table;
}

Per la parte Flutter invece:

 
class BoardWidget extends StatelessWidget {
final List<String> squares;
final Function(int i) squareClick;

const BoardWidget(this.squares, this.squareClick, {Key? key}) : super(key: key);

SquareWidget renderSquare(i) {
return SquareWidget(squares[i], () => squareClick(i));
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [renderSquare(0), renderSquare(1), renderSquare(2)],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [renderSquare(3), renderSquare(4), renderSquare(5)],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [renderSquare(6), renderSquare(7), renderSquare(8)],
)
],
);
}
}

Da notare che per React in questo caso il componente è descritto nel tutorial come una classe che estende React.Component, questo indica che il componente è statefull (in realtà potrebbe essere descritto come stateless ma non voglio modificare quanto descritto nel tutorial).
Anche in questo caso, come per il componente 
Square, la differenza è minima, i widget Column e Row di Flutter di default prendono tutto lo spazio disponibile nella loro dimensione principale quindi per centrare è necessario che la property mainAxisSize sia settanta per entrambi i tipi di widget con il valore MainAxisSize.min

Game

Il componente Game è il più complesso, al contrario di quelli precedenti è di tipo statefull per entrambi i framework.
Partiamo dal codice 
React:


class Game extends React.Component {

constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}

handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}

jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}

render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);

const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});

let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}


Per Flutter l'equivalente è:


class GamePage extends StatefulWidget {
const GamePage({Key? key}) : super(key: key);

@override
State<GamePage> createState() => _GamePageState();
}

class _GamePageState extends State<GamePage> {
List<String?> squares = [];
int stepNumber = 0;
bool xIsNext = true;
List<GameState> history = [GameState()];

@override
Widget build(BuildContext context) {
GameState current = history[stepNumber];
String? winner = calculateWinner(current.squares);
String status = "";
if (winner != "") {
status = "Winner: $winner";
} else {
status = "Next player: ${xIsNext ? "X" : "O"}";
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BoardWidget(current.squares, handleClick),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(status),
...history.map((step) {
int move = history.indexOf(step);
String desc = move != 0 ? 'Go to move #$move' : 'Go to game start';
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
if (move > 0) Text(move.toString()),
ElevatedButton(
child: Text(desc),
onPressed: () {
jumpTo(move);
},
),
],
),
);
}).toList()
],
)
],
),
),
);
}

void handleClick(int i) {
var historySlice = history.sublist(0, stepNumber + 1);
var current = historySlice.last;
var squares = current.squares.sublist(0);
if (calculateWinner(squares) != "" || squares.length < i) {
return;
}
squares[i] = xIsNext ? "X" : "O";
setState(() {
history = [...historySlice, GameState(squares: squares)];
stepNumber = historySlice.length;
xIsNext = !xIsNext;
});
}

void jumpTo(int step) {
setState(() {
stepNumber = step;
xIsNext = step % 2 == 0;
});
}

String calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (int i = 0; i < lines.length; i++) {
int a = lines[i][0];
int b = lines[i][1];
int c = lines[i][2];
if (squares[a] != "" && squares[a] == squares[b] && squares[a] == squares[c]) {
return squares[a];
}
}
return "";
}
}

class GameState {
List<String> squares;

GameState({this.squares = const ["", "", "", "", "", "", "", "", ""]});
}


In entrambi i casi viene inizializzato il componente con uno stato iniziale. Per React questo viene fatto nel constructor dove tutte le variabili relative ad esso sono definite all'interno della variabile state. Per il widget Flutter vediamo che c'è una divisione del widget in due classi, GamePage che definisce il widget e _GamePageState che si occupa della renderizzazione e di gestirne lo stato (al suo interno sono definite come proprietà le variabili che in React abbiamo inserito dentro l'oggetto state).

Notare che se avessimo avuto in ingresso delle proprietà statiche queste sarebbero state definite per il componente React all'interno della variabile props, in Flutter queste sarebbero state date in ingresso al costruttore della classe GamePage.

L'analogia più forte è nella gestione del cambio stato, entrambi lanciano il metodo setState per modificarlo, questo si occupa dell'aggiornamento dei soli componenti che necessitano di essere renderizzati nuovamente.

Conclusioni

Come abbiamo visto entrambi hanno la divisione netta tra componenti stateless e statefull, entrambi ottimizzano la renderizzazione ridisegnando un componente solo quando è necessario.

Da notare però che l'approccio che sfrutta il setState è poco usato per i progetti più avanzati.

Per creare un applicativo Flutter di buon livello è consigliabile utilizzare la libreria provider che permette di fare una divisione netta tra viste (gestita solo con widget stateless) e controllore (gestita tramite providers che ne tengono la logica). Allo stesso modo React sfrutta la libreria redux per ottenere un risultato simile.

Spero che questo articolo vi sia stato d'aiuto per farvi intuire che le due tecnologie sono molto simili e che la scelta su quale utilizzare dovrebbe essere data solo dalle necessità del vostro progetto e che l'eventuale studio non dovrebbe spaventarvi.