[Dart / Flutter] - Você sabe ler/consumir arquivos locais no Dart e exibir em seu App Flutter?
Contexto
Bom, é muito comum em vários contextos de desenvolvimento trabalharmos com requisições http, onde temos contato em praticamente 100% das vezes com o famoso formato JSON.
Dito isto, vão ter situações em que não vamos querer algo que seja consumido de uma API da internet (ou que simplesmente não exista pela internet), algo que deve servir para uma situação muito específica em sua aplicação e que não terá a necessidade de atuar com os métodos http (GET, PUT, POST, PATCH e DELETE). Nós só vamos querer algo estático, apenas para ser exibido, que tenha uma certa organização (ex. chave-valor) e sem precisar de acesso à internet.
Bom, se for o caso, provavelmente você já deve ter pensado (ou não) em escrever (ou gerar via scripts) um arquivo no formato JSON para ser consumido localmente em sua aplicação.
Beleza... Mas, como?
Arquivo local e configurações
Arquivo de exemplo:
{
"name": "Love Live! Superstar!!",
"logo": "https://upload.wikimedia.org/wikipedia/commons/0/00/Love_Live%21_Superstar%21%21_English_logo.png",
"description": "Love Live! Superstar!! (ラブライブ!スーパースター!! Rabu Raibu! Sūpāsutā!!)",
"themeSongs": [
{
"name": "START!! True dreams",
"url": "https://love-live.fandom.com/wiki/START!!_True_dreams"
},
{
"name": "WE WILL!!",
"url": "https://love-live.fandom.com/wiki/WE_WILL!!"
}
],
"seasonsAiredDates": [
"07-11-2021",
"07-17-2022"
]
}
Configuração no pubspec.yaml:
assets:
- lib/core/database/anime.json
Tendo seu arquivo JSON guardado em algum lugar do seu projeto e especificando seu respectivo caminho no pubspec.yaml, podemos separar uma classe pra trabalhar no consumo do conteúdo desse arquivo e uma classe que fará a adaptação do conteúdo que estamos consumindo.
import 'dart:convert';
class Anime {
final String name;
final String logo;
final String description;
final List<String> seasonsAiredDate;
final List<AnimeThemeSongs> themeSongs;
Anime({
required this.name,
required this.logo,
required this.description,
required this.seasonsAiredDate,
required this.themeSongs,
});
factory Anime.fromJson(dynamic map, String group) {
Map<String, dynamic> decodedJson;
try {
decodedJson = jsonDecode(map);
} catch (e) {
decodedJson = map;
}
return Anime(
name: decodedJson['name'],
logo: decodedJson['logo'],
description: decodedJson['description'],
seasonsAiredDate: List<String>.from(decodedJson['seasonsAiredDate']),
themeSongs: List<AnimeThemeSongs>.from(
decodedJson['themeSongs']?.map((x) => AnimeThemeSongs.fromJson(x))),
);
}
}
Basicamente, a classe que vai adaptar o conteúdo contem os exatos mesmos campos e com as tipagens corretas.
Nesta classe, criamos um factory
para o método .fromJson, que será o método utilizado na classe de "serviço" para fazer a adaptação. Com isso, transformamos aquela String retornada em um Map<String, dynamic>.
Usando rootBundle
no Flutter
O Flutter oferece uma variável dentro da biblioteca services, chamada de rootBundle
. Basicamente, é uma variável do tipo AssetBundle
, responsável por estabelecer toda a comunicação com os assets da sua aplicação que estão declarados no campo assets do arquivo pubspec.yaml.
A classe de consumo (digamos que seja nosso "serviço"), será responsável por fornecer o conteúdo do arquivo local já adaptado à classe de adaptação criada anteriormente, para que seja possível exibir esse conteúdo apenas chamando pelos atríbutos da classe.
import 'package:flutter/services.dart';
O AssetBundle possui um método chamado loadString
, que funciona de forma assíncrona. Esse método vai buscar todo o conteúdo do arquivo do caminho específicado e vai retornar ele como uma String.
class AnimeService {
Future<Anime> getAnime() async {
final response =
await rootBundle.loadString('lib/core/database/anime.json');
final decoded = jsonDecode(response);
return Anime.fromJson(decoded);
}
}
Exibindo o conteúdo
Pronto! Agora basta criarmos alguma forma de estabelecer uma conexão entre o serviço e o nosso front-end da aplicação.
class AnimeController {
final anime = ValueNotifier<Anime>(Anime());
Future<void> fetchAnime() async {
final service = AnimeService();
final fetchedAnime = await service.getAnime();
anime.value = anime;
}
}
import 'package:flutter/material.dart';
class AnimePage extends StatefulWidget {
const AnimePage({super.key});
@override
State<AnimePage> createState() => _AnimePageState();
}
class _AnimePageState extends State<AnimePage> {
final controller = AnimeController();
@override
void initState() {
controller.fetchAnime();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.pink,
elevation: 0,
title: const Text(
'Anime',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
body: ValueListenableBuilder(
valueListenable: controller.anime,
builder: (context, state, child) {
return SizedBox(
height: MediaQuery.of(context).size.height * .5,
child: Center(
child: Text(controller.anime.name),
),
);
});
}
}