Primeiros passos com Flutter — Parte II
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.
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:
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:
That's all folks!
É 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.