Arquitetura limpa usando o Flutter Bloc

Tempo de leitura: 15 minutes

Em todos os aplicativos de software, seguimos um padrão de design para arquitetar as coisas. Sem manter uma arquitetura adequada, inevitavelmente encontraremos vários bugs e complexidades ao integrar novos recursos em um desenvolvimento futuro. Portanto, para resolver esses problemas, temos diferentes tipos de padrões de design no Flutter MVP, MVVM, MVC, Bloc, etc.

Agenda:

Desenvolvimento de um pequeno aplicativo usando um bloc centrado no conceito de escola. Esse aplicativo facilita a criação e a atualização de várias escolas, com o recurso adicional de gerenciar alunos em cada escola.

Acredito que você não apenas aprenderá o padrão Bloc com esta publicação, mas também aprenderá a estruturar todo o projeto para usar utilitários, widgets personalizados, mixins etc.

Observação: Antes de prosseguir, espero que você tenha algum conhecimento sobre widgets, como fazer chamadas HTTP e a linguagem de programação dart.

O que é o padrão Bloc?

O Flutter Bloc é uma ferramenta de gerenciamento de estado. Ele é usado para gerenciar o estado de um aplicativo. Internamente, o Bloc usa fluxos para lidar com o fluxo de dados da UI para o Bloc e do Bloc para a UI.

Para obter mais informações, visite o site oficial https://bloclibrary.dev/#/gettingstarted

 

Primeiros passos:

Vamos discutir um pouco sobre o Flutter bloc antes de mergulhar no código. Abaixo está o fluxograma que representa a arquitetura do padrão bloc. Estruturaremos nosso aplicativo somente nesse formato

Interação entre UI e Bloc?

Para gerenciar isso, temos duas coisas principais envolvidas, ou seja, event e stade

Event: Um event é um tipo de acionador da interface do usuário, como a interação do usuário ou a navegação para outra tela. Quando um evento é acionado, ele informa ao bloco para realizar algumas operações, como buscar dados do servidor.

Stade: Quando o bloco tiver os dados, ele emitirá dados/erros no formato de estado para os widgets do bloco.

 


Widgets Bloc:

A biblioteca provides Bloc widgets para lidar com a programação reativa

Bloc Provider/ Multi Bloc Provider:

O Bloc Provider terá acesso a seus filhos abaixo. Abaixo está a maneira de inicializá-lo

BlocProvider(
  create: (BuildContext context) => SchoolBolc()
  child : ...
)

Se você tiver vários blocos, poderá usar o provedor Multi Bloc, conforme mostrado abaixo

MultiBlocProvider(
      providers: [
        BlocProvider(create: (context) => SchoolBloc()),
        BlocProvider(create: (context) => TodoListBloc()),
      ],
      child: ...
)

Bloc Listener :

Com esse widget, você pode ouvir diferentes estados que são emitidos pelo bloco. Esse widget não reconstrói a exibição, ele serve apenas para ouvir eventos para mostrar uma barra de lanches, caixas de diálogo, navegar ou abrir a página.

BlocListener<SchoolBloc, ShoolState>(
  listenWhen : (context, state) {
    return state.isSuccess;
  }
  listener: (context, state) {
    // Mostrar Dialog, Snack Bar ou Banner
  },
  chield: ....
)

Bloc Builder :

Usando o Bloc Builder, você pode reconstruir os widgets filhos com base no estado. Ele tem dois fechamentos: buildWhen e builder.

Com base no estado, você pode escrever uma instrução sobre a necessidade de reconstrução ou não.

BlocBuilder<SchoolBloc, ShoolState>(
  buildWhen : (context, state) {
    return state.schoolStateType == SchoolDataLoadedType.schools;
  }
  builder: (context, state) {
    if(state is SchoolInfoInitial || state is SchoolInfoLoading) {
       return circularLoader();
    } else if (state is SchoolInfoLoaded) {
       return _buildRegisteredSchools(state.schools);
    } else if (state is SchoolDataError) {
       return ExceptionView(state.errorStateType);
    } else {
      return emptyMessage();
    }
  });

 

Bloc Consumer :

O Bloc Consumer é um widget premium que terá os recursos do BlocBuilder e do BlocListener

BlocConsumer<SchoolBloc, ShoolState>(
  listener: (context, state) {
    /// Mostra Dialog, Snack Bar ou Banner
  },
  listenWhen: (context, state) {
    return state.isSucess;
  },
  buildWhen: (context, state) {
    return state.schoolStateType == SchoolDataLoadedType.schools;
  }
  builder: (context, state) {
    if(state is SchoolInfoInitial || state is SchoolInfoLoading) {
       return circularLoader();
    } else if (state is SchoolInfoLoaded) {
       return _buildRegisteredSchools(state.schools);
    } else if (state is SchoolDataError) {
       return ExceptionView(state.errorStateType);
    } else {
      return Conteiner();
    }
  });

 

Bloc Selector:

Esse widget permite filtrar a resposta do estado, conforme mostrado abaixo

BlocSelector<SchoolBloc, ShoolState, bool>(
  selector: (state) => state.schools.length > 0,
  builder: (context, state) {
    return Conteiner(
    color: state ? Colors.green : Colors.red,
    child: ....);
  })
);

 

Usando o Flutter Bloc em um projeto em tempo real:

Neste projeto, vamos usar o banco de dados em tempo real do Firebase para executar operações CRUD usando APIs de descanso.

Conforme discutido anteriormente, trabalharemos no conceito de criação de escolas. Abaixo estão os principais recursos desse aplicativo

  • Criação de uma escola
  • Atualização de mais alguns detalhes de uma escola
  • Adição e atualização de alunos em uma escola.

Neste blog, a criação e a atualização de uma escola serão discutidas no restante do código.

Você pode encontrar o código completo no repositório git => (Link)

Vamos criar os diretórios na pasta lib

ui: Nada mais é do que uma camada de apresentação, que contém toda a lógica da interface do usuário do aplicativo.

bloc: modelo de visualização, que tratará de todas as operações comerciais e atuará como mediador entre a interface do usuário e os dados.

repository: Representa um manipulador de dados, que converterá os dados em nossos modelos de classe necessários.

models: Representam a estrutura dos dados usados no aplicativo. Os modelos geralmente incluem métodos para serializar dados em formatos como JSON ou XML, bem como para desserializar.

O Dart é uma linguagem estaticamente tipada, e o uso de modelos oferece segurança de tipo ao trabalhar com dados

services: Nada além de um provedor de dados, ele cria um canal entre o aplicativo e o servidor e faz todas as chamadas de rede a partir daqui.

widgets: Não utilizaremos diretamente os componentes Material/Cupertino, pois precisaremos personalizá-los com base na experiência do usuário. Todos os componentes personalizados serão colocados nesse diretório.

mixins: em todo aplicativo que tivermos código reutilizável, colocaremos esse código na classe mixin e o usaremos em todo o aplicativo.

Ao contrário da herança, em que uma subclasse só pode herdar de uma superclasse, uma classe pode usar vários mixins.

utils: A pasta utils normalmente contém código que não pertence a nenhum recurso ou domínio específico, mas fornece funcionalidade comum que pode ser usada em todo o projeto. Por exemplo, constantes, enums, etc.

 

Abaixo estão as dependências que usaremos no aplicativo (Principais)

dependencies:
  go_router: ^13.2.3
  loader_overlay: ^4.0.0
  fluttertoast: ^8.2.4
  dio: ^5.4.2+1
  equatable: ^2.0.5
  flutter_bloc: ^8.1.5

dev_dependencies:
  json_serializable: ^6.7.1
  build_runner: ^2.4.9

 

Telas da interface do usuário (camada de apresentação) :

Vamos criar um diretório de schools na pasta ui e, dentro dele, criar arquivos dart, conforme mostrado abaixo

Em schools.dart, exibiremos uma lista de escolas junto com um botão de ação flutuante para create school, conforme mostrado abaixo

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

  @override
  State<Schools> createState() => _SchoolsState();
}

class _SchoolsState extends State<Schools> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Schools'),
      ),
      floatingActionButton: FloatingActionButton.extended(onPressed: () {}, label: Text('Create School'), icon: Icon(Icons.add)),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Registered Schools:', style: Theme.of(context).textTheme.titleMedium),
          Expanded(child: _buildRegisteredSchools([])),
        ],
      ).screenPadding(),
    );
  }

  Widget _buildRegisteredSchools(List schools) {
    if (schools.isEmpty) return const Center(child: Text('No Schools Found, Create a new School', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)));

    return SizedBox(
      width: DeviceConfiguration.isMobileResolution ? null : MediaQuery.of(context).size.width / 3,
      child: ListView.separated(
          itemCount: schools.length,
          itemBuilder: (context, index) {
            var school = schools.elementAt(index);
            return ListTile();
          },
          separatorBuilder: (BuildContext context, int index) => Divider()),
    );
  }
}

Os resultados da interface do usuário serão os mostrados abaixo

 

No momento, não há escolas para mostrar, portanto, precisamos criar uma escola primeiro. Portanto, na torneira de criar escola, vamos criar uma caixa de diálogo com três entradas Nome da escola, País e Local em create_update_school.dart

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

  @override
  State<CreateOrUpdateSchool> createState() => _CreateOrUpdateSchoolState();
}

class _CreateOrUpdateSchoolState extends State<CreateOrUpdateSchool> {

  final TextEditingController schoolNameCtrl = TextEditingController();

  final TextEditingController locationCtrl = TextEditingController();

  static const List<String> countries = ['India', 'USA', 'UK', 'Russia', 'Dubai', 'China', 'Japan'];

  final GlobalKey<FormState> formKey = GlobalKey<FormState>();

  String? selectedCountry;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
          child: Text('Create School', style: Theme.of(context).textTheme.titleMedium),
        ),
        const Divider(),
        _buildFrom(),
        const Divider(),
        _buildButtons()
      ],
    );
  }

  Widget _buildFrom() {
    return Form(
      key: formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Wrap(
          spacing: 10,
          runSpacing: 20,
          children: [
            TextFormField(controller: schoolNameCtrl, decoration: InputDecoration(label: Text('School Name'), suffixIcon: const Icon(Icons.school), border: const OutlineInputBorder())),
            DropdownButtonFormField(
              hint: Text('Select Country', style: TextStyle(fontSize: 17, color: Colors.black.withOpacity(0.5))),
              items: countries.map((e) => DropdownMenuItem(child: Text(e), value: e)).toList(),
              onChanged: (val) {},
              value: selectedCountry,
              style: const TextStyle(fontWeight: FontWeight.w100, color: Colors.black),
              decoration: const InputDecoration(border: OutlineInputBorder()),
            ),
            TextFormField(controller: locationCtrl, decoration: InputDecoration(label: Text('Location Name'), suffixIcon: const Icon(Icons.location_on), border: const OutlineInputBorder())),
          ],
        ),
      ),
    );
  }

  Widget _buildButtons() {
    return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Align(
          alignment: Alignment.centerRight,
          child: Wrap(spacing: 10, alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.end, children: [ElevatedButton(onPressed: () {}, child: Text('Cancel')), ElevatedButton(onPressed: () {}, child: Text('Create'))]),
        ));
  }
}

Abra a caixa de diálogo ao tocar em Create School e veja a caixa de diálogo abaixo

Até o momento, implementamos apenas uma interface de usuário simples. Agora, precisamos implementar a lógica de negócios usando o bloco, mas antes disso precisamos discutir um pouco sobre modelos.

 

Models:

O Dart é uma linguagem de tipo restrito, a comunicação entre UI, bloc, data handler será apenas na instância da classe. Também podemos usar Map, mas isso não é recomendado. Portanto, crie uma classe SchoolModel conforme mostrado abaixo, pois usaremos esse objeto em todo o aplicativo.

class SchoolModel {
  final String id;
  final String schoolName;
  final String country;
  final String location;
  
  SchoolModel(this.id; this.schoolName, this.country, this.location);
}

Observação: O modelo acima serve para manter as informações. Encaminhando a discussão. Discutiremos mais sobre serialização, setter e getter de JSON na seção Manipulador de rede (Repositório).

 

Bloc (View Model):

Vamos criar os três arquivos abaixo no diretório bloc

Conforme discutido anteriormente, o evento serve para acionar o bloco, como na batida de criar escola, e o estado fornece os resultados para a interface do usuário, como fornecer escolas criadas.

Vamos nos aprofundar em cada arquivo e ver como podemos usá-los…!!!

school_event.dart:

Vamos criar dois eventos createOrUpdateSchool que informa ao bloco que o usuário criou uma escola e SchoolsDataEvent para buscar as escolas criadas

part of 'school_bloc.dart';

@immutable
abstract class SchoolEvent {}

class CreateOrUpdateSchoolEvent extends SchoolEvent {
  final SchoolModel school;
  final bool isCreateSchool;

  CreateOrUpdateSchoolEvent(this.school, {this.isCreateSchool = true});
}
class SchoolsDataEvent extends SchoolEvent {
  SchoolsDataEvent();
}

Na classe createOrUpdateSchool acima, há uma propriedade school que contém as informações da escola e o valor do sinalizador isCreateSchool indica se a escola deve ser criada ou atualizada.

Podemos criar eventos diferentes com base na necessidade de buscar escolas criadas, alunos criados etc. Com base no evento, o bloco responderá. Para todas essas classes de eventos, o tipo é SchoolEvent, portanto, criamos uma classe abstrata.

school_state.dart:

Diferentes estados são necessários aqui. Com base em um estado, os widgets de bloco exibem os dados. Por exemplo, se precisarmos exibir as escolas, primeiro precisamos mostrar o carregador enquanto buscamos os dados do servidor; nesse momento, o estado é SchoolInfoLoading; depois que os dados forem buscados, precisamos exibir os dados que terão um estado diferente, ou seja, SchoolsInfoLoaded. Em outros casos, também haverá falhas; se houver um problema de conexão com o servidor/internet, precisaremos mostrar um erro compreensível para o usuário na interface do usuário; nesse momento, usaremos o estado SchoolDataError.

part of 'school_bloc.dart';

enum SchoolDataLoadedType {schools, school, students, student}

abstract class SchoolState extends Equatable{
  
  final SchoolDataLoadedType schoolStateType;
  
  const SchoolState(this.schoolStateType);
}


/// Initial
class SchoolInfoInitial extends SchoolState {

  const SchoolInfoInitial(super.schoolStateType);

  @override
  List<Object?> get props => [];

}

/// For showing loader
class SchoolInfoLoading extends SchoolState {

  SchoolInfoLoading(super.schoolStateType);

  @override
  List<Object?> get props => [super.schoolStateType];

}

/// To show the schools
class SchoolsInfoLoaded extends SchoolState {

   final List<SchoolModel> schools;

   const SchoolsInfoLoaded(SchoolDataLoadedType schoolStateType, this.schools) : super(schoolStateType);

  @override
  List<Object?> get props => [super.schoolStateType, schools];
}

/// To show the error
class SchoolDataError extends SchoolState {

  final DataErrorStateType errorStateType;

  SchoolDataError(super.schoolStateType, this.errorStateType);

  @override
  List<Object?> get props => [schoolStateType, errorStateType];

}

No código acima, declaramos a propriedade enum SchoolDataLoadedType , que é usada no construtor de blocos para comparar se a interface do usuário precisa ser renderizada ou não. Para todo o conceito de escola, estamos usando apenas um bloco.

A seguir, discutiremos como implementar esse estado e esse evento na tela.

school_bloc.dart:

Vamos criar um SchoolBloc a partir do código abaixo, que estende o Bloc com tipos genéricos como SchoolEvent e SchoolState, criados na seção acima.

Precisamos passar o estado inicial para o construtor do bloco, pois os widgets do bloco serão iniciados com o estado SchoolInfoInitial. Portanto, estamos passando SchoolInfoInitial como o estado aqui.

Além disso, precisamos passar o repositório, que é conhecido como Data handler (manipulador de dados), sobre o qual falaremos mais adiante

Declare uma variável de instância schools para armazenar as escolas criadas nela

part 'school_event.dart';
part 'school_state.dart';

class SchoolBloc extends Bloc<SchoolEvent, SchoolState> {

  final SchoolRepository repository;
  var schools = <SchoolModel>[];

  SchoolBloc() : super(SchoolInfoInitial(SchoolDataLoadedType.schools)) {
    
    on<SchoolsDataEvent>(fetchSchools);
    on<CreateOrEditSchoolEvent>(createOrUpdateSchool);  
  }

  /// Fetch created schools
  Future<void> fetchSchools(SchoolsDataEvent schoolEvent, Emitter<SchoolState> emit) async {
    try {
    
    } catch (e, s) {

    }
  }

  /// Create or update a school
  Future<void> createOrUpdateSchool(CreateOrEditSchoolEvent schoolEvent, Emitter<SchoolState> emit) async {
    try {

    } catch (e, s) {

    }
  }
}

Para usar esse bloc, primeiro precisamos inicializá-lo, conforme mostrado abaixo

BlocProvider<SchoolBloc>(
  create: (BuildContext context) => SchoolBloc(SchoolRepository()),
  child: ...
)

Agora podemos acessar o SchoolBloc em seus filhos decendentes.

Vamos buscar as escolas existentes usando o bloco. Nas seções da interface do usuário (discutidas anteriormente), criamos o arquivo schools.dart para exibir as escolas e, nesse arquivo, adicionamos SchoolsDataEvent ao bloc, conforme mostrado abaixo

@override
void initState() {
  BlocProvider.of<SchoolBloc>(context).add(SchoolsDataEvent());
  super.initState();
}

Agora, isso acionará os métodos loadSchools no bloco ao corresponder ao tipo de evento SchoolsDataEvent e, novamente, o bloco solicitará o repositório (Data Handler) para as escolas, conforme mostrado a seguir

Future<void> loadSchools(SchoolsDataEvent event, Emitter<SchoolState> emit) async {

    try {
      schools = await repository.fetchSchools();
    } catch (e, s) {

    }
  }

Na seção do manipulador de rede, falaremos sobre o repositório.

O método repository.fetchSchools() fornece as escolas existentes do provedor de rede, agora precisamos emitir esses dados para a interface do usuário usando a propriedade emit, conforme mostrado abaixo

Future<void> fetchSchools(SchoolsDataEvent schoolEvent, Emitter<SchoolState> emit) async {
  const SchoolState = SchoolsDataLoadedType.schools;
    
  emit(SchoolInfoLoading(schoolState);
    
  try {
    var schools = await repository.fetchSchools().
    emit(SchoolsInfoLoaded(schoolState, schools);
  } catch (e, s) {
    emit(SchoolDataError(schoolState, DataErrorStateType.noInternet));
  }
}

No código acima, estamos emitindo o estado em três lugares,

  • A primeira emissão é para dizer ao widget de bloco para exibir o carregador.
  • O segundo emit é para emitir escolas a partir da resposta do servidor.
  • A terceira emissão é para emitir um erro para o widget de bloco.

Qualquer um dos estados será emitido para o widget de bloco, mas, para lidar com isso, precisamos implementar o widget de bloco em nossa tela de interface do usuário(schools.dart).

return BlocBuilder<SchoolBloc, SchoolState>(
   buildWhen: (context, state) {
      return state.schoolStateType == SchoolDataLoadedType.schools;
   },
   builder: (context, state) {
     if (state is SchoolInfoInitial || state is SchoolInfoLoading) {
        return circularLoader();
     } else if (state is SchoolsInfoLoaded) {
       return _buildRegisteredSchools(state.schools);
     } else if(state is SchoolDataError) {
       return ExceptionView(state.errorStateType);
     } else {
       return Container();
     }
 });

Temos diferentes tipos de widgets de bloco com base nos requisitos que usaremos. Aqui, o BlocBuilder é necessário para exibir as escolas, por isso o utilizamos.

No callback buildWhen , , estamos comparando se schoolStateType é Schools ou não; se for Schools, então retornaremos um valor verdadeiro e o construtor renderizará novamente o BlocBuilder.

O estado inicial é SchoolInfoLoading , portanto, ele executa o carregador circular. Depois que os dados são carregados no bloco, ele emite SchoolsInfoLoaded e executa a saída _buildRegisteredSchools , como mostrado abaixo

Vamos criar a escola da mesma forma

Ao tocar no botão create school (criar escola ), o site invoca o método createOrUpdateSchool no bloco passando CreateOrEditSchoolEvent para o bloc. Podemos passar o evento chamando o método add no bloco, conforme mostrado abaixo.

Usando o contexto, podemos acessar as propriedades do bloc read, watch …

Se o evento foi criado, estamos passando uuid(Universally Unique IDentifier) como id ou então passando o id da escola existente para atualizar a mesma escola

context.read<SchoolBloc>().add(CreateOrUpdateSchoolEvent(SchoolModel(
      schoolNameCtrl.text.trim(),
      selectedCountry!,
      locationCtrl.text.trim(),
      isCreateSchool ? HelperMethods.uuid : widget.school!.id,
),
      isCreateSchool : isCreateSchool
));

Agora, no bloco, o método createOrUpdateSchool foi invocado e tem dados da escola . Precisamos passar esses dados para o manipulador de dados (Repositório), que solicitará ao servidor a criação de dados.

Future<void> createOrUpdateSchool(
  CreateOrUpdateSchoolEvent event, Emitter<SchoolState> emit) async {

  const schoolState = SchoolDataLoadedType.schools;

   try {
      navigatorKey.currentContext?.loaderOverlay.show();
      
      var createdOrUpdatedSchool = await repository.createOrEditSchool(event.school);

      schools = List.from(schools);

      if (!event.isCreateSchool) {
        var index =
        schools.indexWhere((school) => school.id == event.school.id);
        if (index != -1) {
          schools[index] = createdOrUpdatedSchool;
        }
      } else {
        schools.add(createdOrUpdatedSchool);
      }

      emit(SchoolsInfoLoaded(schoolState, schools));
    } catch (e, s) {
      /// Tratar o erro
    } finally {
      navigatorKey.currentContext?.loaderOverlay.hide();
    }
  }

Quando a escola for criada ou atualizada com êxito, ela será adicionada ou atualizada na lista de matriz de escolas, conforme mostrado na lógica acima

No código acima, você pode encontrar a propriedade navigatorKey, que é uma chave global declarada no GoRouter, pois todo o projeto usa o GoRouter para navegar entre as páginas. A partir dessa chave, podemos obter o contexto atual.

static final GoRouter router = GoRouter(
    navigatorKey: navigatorKey,

Portanto, costumávamos mostrar o carregador passando esse contexto para o pacote loader_overlay , conforme mostrado abaixo

navigatorKey.currentContext?.loaderOverlay.show();
navigatorKey.currentContext?.loaderOverlay.hide();

Ele tem duas opções: mostrar e ocultar, para exibir e ocultar o carregador.

 

Repositório (manipulador de dados):

O repositório gerencia a preparação e a execução da solicitação, bem como a desserialização dos dados de resposta em instâncias de classe que representam dados estruturados.

Aqui podemos definir o ponto de extremidade, o tipo de método e converter a instância de classe em Map (serialização) para preparar os parâmetros de corpo/consulta de uma solicitação.

A resposta será convertida em classes de modelo (dados estruturados) usando JSON e o conceito de serialização no Flutter.

Vamos criar um arquivo school_repository.dart no diretório do repositório e criar uma classe abstrata SchoolRepo e uma classe SchoolRepository

abstract class SchoolRepo {
  Future<List<SchoolModel>> fetchSchools();
  Future<SchoolModel> createOrEditSchool(SchoolModel school);
}

Implemente essa classe abstrata no SchoolRepository

class SchoolRepository with BaseService implements SchoolRepo {

  @override
  Future<List<SchoolModel>> fetchSchools() async {
     List<SchoolModel> schools = <SchoolModel>[];
     var response = await makeRequest(url: '${Urls.schools}.json');
     if(response is Map) {
       schools = response.entries.map<SchoolModel>((json) => SchoolModel.fromJson(json.value)).toList();
     }
     return schools;
  }

  @override
  Future<SchoolModel> createOrEditSchool(SchoolModel school) async {

    Map<String, dynamic> body = {school.id : school.toJson()};

    var response = await makeRequest(url: '${Urls.schools}.json', body: body, method: RequestType.patch);
    if(response != null && response is Map && response.keys.isNotEmpty) {
      school = SchoolModel.fromJson(response[response.keys.first]);
    }

    return school;
  }

}

Vamos adicionar um mixin BaseService, que é a classe que lhe dará acesso para solicitar o servidor. Discutiremos isso na seção sobre o provedor de dados.

Na seção de modelos (que foi discutida anteriormente), criamos uma classe SchoolModel. A resposta do provedor de dados estará em Map ou List<Map> , portanto, precisamos convertê-la no SchoolModel necessário, mas nosso SchoolModel não tem essa capacidade.

Podemos fazer isso usando a serialização JSON , vamos modificar nosso SchoolModel

import 'package:json_annotation/json_annotation.dart';

part 'school_model.g.dart';

@JsonSerializable()
class SchoolModel {
  SchoolModel(this.schoolName, this.country, this.location, this.id);

  @JsonKey(required: true)
  final String schoolName;

  final String country;
  final String location;
  final String id;

  factory SchoolModel.fromJson(Map<String, dynamic> json) =>
      _$SchoolModelFromJson(json);

  Map<String, dynamic> toJson() => _$SchoolModelToJson(this);
}

Execute o comando abaixo para gerar o arquivo g dart

dart run build_runner build – delete-conflicting-outputs

Agora você terá acesso aos métodos toJson e fromJson

if (response is Map) {
   schools = response.entries.map<SchoolModel>(
     (json) => SchoolModel.fromJson(json.value)).toList();
   })
  );
}

Aqui estará no formato Map, portanto a resposta será iterada usando entradas

Usando from fromJson, converta os dados em SchoolModel. Se houver alguma exceção, o erro será tratado diretamente no próprio bloco.

Serviço básico (provedor de dados) :

O serviço de base é responsável por enviar a solicitação ao servidor.

Os tokens de autorização, as urls básicas e outros cabeçalhos serão adicionados aqui.

Temos diferentes pacotes para fazer chamadas de rede, como http, dio, etc. Neste projeto, estamos usando o Dio. Abaixo está o código de como as solicitações e a resposta serão tratadas.

import 'package:dio/dio.dart';
import 'dart:async';

mixin BaseService {
  Future<dynamic> makeRequest<T>(
      {required String url,
      String? baseUrl,
      dynamic body,
      String? contentType,
      Map<String, dynamic>? queryParameters,
      Map<String, String>? headers,
      RequestType method = RequestType.get,
      Map<String, dynamic> extras = const {}}) async {

    dio.options.baseUrl = baseUrl ?? Urls.baseUrl;
    dio.options.extra.addAll(extras);

    if (headers != null) dio.options.headers.addAll(headers);

    Response response;
    switch (method) {
      case RequestType.get:
        if (queryParameters != null && queryParameters.isNotEmpty) {
          response = await dio.get(
            url,
            queryParameters: queryParameters,
          );
          return response.data;
        }
        response = await dio.get(url);
        return response.data;
      case RequestType.put:
        response =
            await dio.put(url, queryParameters: queryParameters, data: body);
        return response.data;
      case RequestType.post:
        response = await dio.post(
          url,
          queryParameters: queryParameters,
          data: body,
        );
        return response.data;
      case RequestType.delete:
        response =
            await dio.delete(url, queryParameters: queryParameters, data: body);
        return response.data;
      case RequestType.patch:
        response = await dio.patch(
          url,
          queryParameters: queryParameters,
          data: body,
        );
    }
    return response.data;
  }
}

Com base no método, o caso de troca será executado. Também podemos substituir a baseurl, adicionar cabeçalhos extras …

widgets:

Na pasta widgets, podemos adicionar nossos widgets personalizados e usá-los globalmente. Por exemplo, neste aplicativo, temos um campo de texto e um menu suspenso em todos os lugares para criar escolas, alunos etc. Portanto, criamos um campo de texto personalizado e um menu suspenso genérico no tema necessário, conforme mostrado abaixo.

Custom Text Field :

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CustomTextField extends StatelessWidget {

  const CustomTextField({Key? key, required this.controller, required this.label, this.suffixIcon, this.validator, this.inputFormatter}) : super(key: key);

  final TextEditingController controller;

  final String label;

  final Icon? suffixIcon;

  final String? Function(String?)? validator;

  final List<TextInputFormatter>? inputFormatter;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      decoration: outlineDecoration(),
      validator: validator,
      inputFormatters: inputFormatter,
    );
  }

  InputDecoration outlineDecoration() {
    return InputDecoration(
      label: Text(label),
      suffixIcon: suffixIcon,
      border: const OutlineInputBorder()
    );
  }
}

Custom Drop down :

import 'package:flutter/material.dart';

class CustomDropDown<T> extends StatelessWidget {

  const CustomDropDown({Key? key, required this.value, required this.items, required this.onChanged, this.hint, this.validator}) : super(key: key);

  final T? value;

  final List<DropdownMenuItem<T>> items;

  final ValueChanged<T?> onChanged;

  final String? hint;

  final String? Function(T?)? validator;

  @override
  Widget build(BuildContext context) {
    return DropdownButtonFormField<T>(
      hint: hint != null ? Text(hint!) : null,
      items: items, onChanged: onChanged,
      value: value,
      validator: validator,
      style: const TextStyle(fontWeight: FontWeight.w100, color: Colors.black),
      decoration: const InputDecoration(
        border: OutlineInputBorder()
      ),
    );
  }
}

mixins:

Os mixins são uma forma de reutilizar o código em várias classes. No conceito de herança, a subclasse pode herdar apenas uma superclasse, mas nos mixins herdamos várias classes usando a palavra-chave with

Em todos os aplicativos existem muitos métodos auxiliares reutilizáveis, podemos colocar todos esses métodos em mixins e usá-los em todos os aplicativos.

Em nosso aplicativo, podemos tornar alguns métodos comuns, por exemplo, diálogos, mensagens vazias, carregadores e validadores.

Dialog mixin :

import 'package:flutter/material.dart';

mixin CustomDialogs {
  void adaptiveDialog(BuildContext context, Widget content) {
    showAdaptiveDialog(
        context: context,
        builder: (context) {
          return Dialog(
              child: SizedBox(
            width: MediaQuery.of(context).size.width / 3,
            child: content,
          ));
        });
  }

  Widget dialogWithButtons(
      {required String title,
      required Widget content,
      required List<String> actions,
      required ValueChanged<int> callBack}) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
          child: Text(title),
        ),
        const Divider(),
        content,
        const Divider(),
        _buildButtons(actions, callBack)
      ],
    );
  }

  Widget _buildButtons(List<String> actions, ValueChanged<int> callBack) {
    return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Align(
          alignment: Alignment.centerRight,
          child: Wrap(
            spacing: 10,
            alignment: WrapAlignment.end,
            crossAxisAlignment: WrapCrossAlignment.end,
            children: List.generate(
                actions.length,
                (index) => ElevatedButton(
                    onPressed: () => callBack(index),
                    child: Text(actions.elementAt(index)))),
          ),
        ));
  }
}

Loaders:

import 'package:flutter/material.dart';

mixin Loaders {

  Widget circularLoader() {
    return const Center(child: CircularProgressIndicator());
  }
}

utils:

A pasta utils normalmente contém código que não pertence a nenhum recurso ou domínio específico, mas fornece funcionalidade comum que pode ser usada em todo o projeto. Para declarar constantes, enums, configuração de dispositivos, etc. Usaremos a pasta utils

 

 

Segue o código completo, com varias modificações e evoluções GitHub (Git)