Primeiros passos com Flutter — Parte II

Bruno Ferrari
Ignus Insights
Published in
7 min readJul 18, 2018

Esse post é uma continuação da primeira parte:

Implementando o Layout

Vamos agora criar nosso layout principal, a listagem, nesse ponto você deve ter algum layout pré construído pelo Android Studio, mas vamos alterá-lo completamente, porém antes de implementar o layout vamos ajustar nosso arquivo main.dart ele deve ficar da seguinte maneira e deve ser responsável, ao menos por hora apenas por inicializar a aplicação.

import 'package:flutter/material.dart';
import 'package:flutter_star_wars/ui/movies/movies_list.dart';
void main() => runApp(new MyApp());class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: MoviesList(title: 'Star Wars Demo'),
);
}
}

Agora sim voltando a implementação do layout da lista vamos criar um novo arquivo .dart movies_list.dart por exemplo, criaremos então nossos widgets aqui.

class MoviesList extends StatefulWidget {
MoviesList({Key key, this.title}) : super(key: key);
final String title;@override
_MoviesListState createState() => new _MoviesListState();
}

Vamos começar pelo código acima, criando nossa primeira tela, ela herda de StatefulWidget o que indica que ela tem um objeto de estado definido que contém campos os quais podem afetar a forma com a qual ela se apresenta na tela. Perceba que temos aqui uma variável que armazena um título, esse título será o título exibido em nossa AppBar quando a tela for invocada.

No mesmo arquivo .dart, vamos declarar outra classe, essa por sua vez irá controlar como recebemos os dados e como vamos exibí-los na tela.

class _MoviesListState extends State<MoviesList> {  final _api = Api();  @override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: FutureBuilder<MoviesWrapper>(
future: _api.fetchMovies(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) print('ERROR: ' + snapshot.error.toString());
return snapshot.hasData
? MoviesListWidget(wrapper: snapshot.data)
: ActivityIndicator();
},
),
);
}
}

Implementamos no código logo acima uma AppBar, perceba que estou utilizando o widget.title para acessar aquela variável de título que passamos no trecho de código anterior. O body receberá qualquer view ou views que estamos querendo apresentar no corpo da tela. Note que aqui estou tratando aquele Future, chamando nossa api e utilizando o builder para saber se devo renderizar um conteúdo ou um erro, nesse caso não estamos exibindo o erro para o usuário, apenas logando no console. Caso a requisição seja um sucesso vamos exibir nossa lista preenchida com os dados recebidos, caso contrário, enquanto a requisição não for um sucesso ficaremos exibindo o loading na tela.

Tanto o MoviesListWidget quanto o ActivityIndicator ainda não existem em nosso código, eles são widgets e vamos implementá-los como componentes separados para serem melhor reaproveitados posteriormente.

Crie um novo arquivo .dart chamado list_widget ou como achar melhor e vamos implementar nossa lista.

Primeiramente vamos implementar nosso indicador de atividade, ou seja nosso loading, crie um novo arquivo .dart e nomei-o como achar mais conveniente, em seguida implemente o layout abaixo

import 'package:flutter/material.dart';class ActivityIndicator extends StatelessWidget {  ActivityIndicator({ Key key }): super(key: key);  @override
Widget build(BuildContext context) =>
Center(
child: Container(
child: CircularProgressIndicator(),
),
);
}

Essa view é bem simples, a função build será a responsável por criar os elementos de UI em nossa tela, durante meu aprendizado em Flutter gostei bastante de consultar algumas sheet cheats de layout tal como essa. Você pode consultá-las para saber melhor como lidar com o posicionamento dos elementos na tela.

E lembre-se que tudo bem se você se sentir assim nos primeiros momentos:

Vamos então para a implementação do layout da nossa listagem, crie um novo arquivo .dart e mãos à obra.

import 'package:flutter/material.dart';
import 'package:flutter_star_wars/components/list_cell.dart';
import 'package:flutter_star_wars/model/movies_wrapper.dart';
class MoviesListWidget extends StatelessWidget {
final MoviesWrapper wrapper;
MoviesListWidget({Key key, this.wrapper}) : super(key: key); @override
Widget build(BuildContext context) {
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemCount: wrapper.movies.length,
itemBuilder: (context, index) {
return ListCell(movies: wrapper.movies, index: index);
});
}
}

No trecho acima estamos utilizando o componente ListView que faz parte do Flutter, colocamos um espaçamento em todas as direções para que crie-se uma margem, o atributo itemCount é muito importante aqui, caso contrário o Flutter não saberá como lidar com sua lista e frequentemente você terá problemas com IndexOutOfRange e outras exceções do tipo. O itemBuilder por sua vez é o elemento chave aqui, ele recebe o que iremos renderizar em cada item da nossa listagem, ou seja, o layout de cada linha.

O ListCell não é um componente padrão do Flutter, vamos ter que implementá-lo, poderíamos facilmente implementar todo o layout da linha alí mesmo encadeando o código, mas estaríamos diante de um grande código macarrônico, difícil de compreender e mais complicado ainda para resolver problemas devido a complexidade, tal como essa imagem abaixo.

Retirado de https://medium.com/flutter-io/out-of-depth-with-flutter-f683c29305a8

Levando isso em consideração vamos criar outro arquivo .dart, sim novamente… E dentro dele vamos implementar nossa célula, o layout que irá ser renderizado em cada linha da nossa listagem.

import 'package:flutter/material.dart';
import 'package:flutter_star_wars/model/movie.dart';
import 'package:flutter_star_wars/ui/movies/movie_details.dart';
class ListCell extends StatelessWidget { final List<Movie> movies;
final int index;
ListCell({this.movies, this.index}); @override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
title: Text(movies[index].title),
onTap: () {
Navigator.push(context,
_onTap(movies[index].title, context, movies[index]));
},
),
Divider(),
],
);
}
_onTap(String title, BuildContext context, Movie movie) {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
MovieDetails(
title: title,
movie: movie,
))
);
}
}

Perceba que todo widget que implementamos a parte herda de StatelessWidget isso garante que ele não tenha um estado mutável, ou seja, a única maneira de modificarmos algo aqui seria com uma outra instância desse widget, diferentemente do que já vimos com o StatefulWidget anteriormente.

Nesse widget estamos recebendo no construtor uma listagem de filmes e um index, note o construtor da classe. Na função build retornamos o layout de nossa célula, primeiramente com uma coluna, que recebe um array de widgets e lá dentro criamos uma instância de ListTile o qual é um componente simples do Flutter que recebe algum título e opcionalmente algum ícone/botão à esquerda ou a direita.

Temos também um atributo onTap o qual lidará com o que deve ser feito quando nossos usuários clicarem naquele ListTile, mas vamos deixar o onTap de lado por enquanto.

Ao rodar a aplicação você deverá ter algo similar:

Tela de listagem após implementação

Implementando o clique

Voltando a implementação do nosso clique, lá no trecho de código anterior para que o código não ficasse tão complexo pra ser compreendido extraí a implementação para uma função auxiliar que pode ser conferida logo abaixo.

_onTap(String title, BuildContext context, Movie movie) {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
MovieDetails(
title: title,
movie: movie,
))
);
}

Aqui é fácil compreender porquê passamos um index no construtor dessa classe, afinal precisamos saber qual elemento da lista estamos lidando neste exato momento, outra coisa a se notar aqui é a falta do tipo de retorno, nesse caso ele fica inferido uma vez que é do mesmo tipo que o Navigator.push retorna. O MaterialPageRoute é o componente padrão de navegação do Flutter que irá navegar para a tela de detalhe, detalhe que esse componente faz a transição de tela tal como padrão no Android, você também pode usar o CupertinoPageRoute para fazer a transição tal como acontece no iOS. Mas falta implementarmos a próxima tela ainda, vamos a ela!

Implementando a tela de detalhe

Essa tela é bem simples, não cheguei a enfeitar muito nosso layout justamente pois pretendo fazer outro post em breve falando somente disso, além do mais queria manter o foco nos primeiros passos aqui.

Vamos criar outro arquivo .dart para a tela de detalhes e implementá-la de acordo com o código abaixo.

import 'package:flutter/material.dart';
import 'package:flutter_star_wars/components/movie_content.dart';
import 'package:flutter_star_wars/model/movie.dart';
class MovieDetails extends StatefulWidget {
MovieDetails({ Key key, this.title, this.movie }) : super(key: key);
final String title;
final Movie movie;
_MovieDetailsState createState() => _MovieDetailsState();
}
class _MovieDetailsState extends State<MovieDetails> { @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: MovieContent(movie: widget.movie),
);
}
}

Não há nada muito diferente aqui do que já fizemos anteriormente, a única mudança é que estamos recebendo um objeto do tipo Movie no construtor.

A parte mais importante do layout aqui fica por conta da classe MovieContent que define como nossa tela de detalhe irá se comportar.

import 'package:flutter/material.dart';
import 'package:flutter_star_wars/model/movie.dart';
class MovieContent extends StatelessWidget { final Movie movie; MovieContent({ this.movie }); @override
Widget build(BuildContext context) =>
movie != null ? _buildContent(context) : _onError();
Widget _buildContent(BuildContext context) {
return Container(
constraints: BoxConstraints.expand(),
padding: const EdgeInsets.all(32.0),
child: Column(
children: <Widget>[
Text(
'Episode: ' + movie.episodeId.toString(),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
),
Text(
'Release Date: ' + movie.releaseDate,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
),
Text(
'Director: ' + movie.director,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
),
Text(
'Producers: ' + movie.producer,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
),
Container(
padding: EdgeInsets.only(top: 16.0),
child: Text(
movie.openingCrawl,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _onError() {
return Container();
}
}

Perceba que aqui estou tratando de forma bem simples quando algo da errado, por exemplo se o movie != null exibimos nosso layout de fato, caso contrário vamos renderizar uma tela em branco, obiviamente poderíamos tratar isso de maneira mais rebuscada e com outros elementos visuais mas vamos nos ater na praticidade por enquanto.

Vamos ter uma tela bem próxima disso no final:

Tela de detalhe do filme

That's all folks!

All done!

É isso aí galera! Se você chegou até aqui é porquê provavelmente conseguiu e aprendeu a lidar com o Flutter e finalmente deu os primeiros passos. Existem muitos materiais bons na internet sobre o Flutter, mas ainda é uma tecnologia nova e em desenvolvimento, o que implica que algumas coisas podem e devem mudar e também melhorar em um futuro próximo.

Por favor não deixem de comentar abaixo caso encontrem algum erro pra ser ajustado ou modificação que precise ser feita além de novas ideias é claro, que são muito bem vindas.

--

--