ورود و عضویت
0
سبد خرید شما خالی است
0
سبد خرید شما خالی است

آموزش پیاده سازی دیتابیس ObjectBox  در فلاتر

0 دیدگاه

ذخیره سازی اطلاعات بصورت محلی داخل اپلیکیشن و یا در یک سرور ابری یکی از الزامات اساسی طراحی و توسعه برنامه های موبایلی می باشد.

انتخاب پایگاه داده مناسب در هر پروژه گاهی اوقات خود به یک چالش اساسی تبدیل میشود.

در فریمورک فلاتر دیتابیس های گوناگونی برای ذخیره سازی اطلاعات وجود دارد که در مقالات قبلی با تعدادی از آنها کار کردیم.

در این مقاله قصد داریم با دیتابیس NoSQL دیگری به نام ObjectBox آشنا شویم و نحوه کار و قابلیت های آن را در محیط برنامه نویسی فلاتر بررسی کنیم.

از ویژگی های مهم پایگاه داده ObjectBox میتوان به سرعت فوق العاده بالاتر آن نسبت به SQLite اشاره کرد که طبق بررسی های بنچمارک میتوانید به سرعتی 10برابر بهتر در عملیات های مهم دست پیدا کنید. همچنین در برابر پایگاه داده محبوب دیگر فلاتر یعنی Hive نیز عملکرد بسیار بهتری از خود نشان داده است.

حجم این دیتابیس بسیار کوچک میباشد و فضایی کمتر از 1مگابایت اشغال میکند.

استفاده حداقلی از منابع حافظه و CPU باعث شده است که در مصرف انرژی باتری گوشی نیز صرفه جویی شود.

نکته بسیار قابل توجه دیگر امکان همگان سازی دیتابیس بین دستگاه های مختلف میباشد.

آموزش دیتابیس ObjectBox
پایگاه داده ObjectBox

نیازمندهای پروژه

پکیج های مورد نیاز در این پروژه را طبق کدهای زیر درون فایل  pubspec.yaml قرار دهید.

name: flutter_objectbox_example
description: Flutter project that shows how to work with ObjectBox database.
publish_to: "none"
version: 1.0.0+1

environment:
  sdk: ">=2.17.6 <3.0.0"

dependencies:
  bloc: ^8.1.0            
            # State management
  cupertino_icons: ^1.0.2

  equatable: ^2.0.3             
      # Value based equality
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.1          
      # Bloc design pattern
  formz: ^0.4.1                    
   # Simplify form registration and validation
  objectbox: ^1.6.0    
               # ObjectBox
  objectbox_flutter_libs: ^1.6.0  
    # Flutter runtime libraries for ObjectBox
  path: ^1.8.1                 
       # Path manipulation
  path_provider: ^2.0.11       
       # Provides paths for commonly used locations and directories

dev_dependencies:

  build_runner: ^2.2.0             
   # Code generation library
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  objectbox_generator: ^1.6.0    
     # ObjectBox Flutter database binding generator

flutter:
  uses-material-design: true

برای این آموزش در بحث مدیریت State نیز از پکیج بلاک استفاده خواهیم کرد.

اگر از قابلیت همسان سازی دیتابیس میخواهید استفاده کنید مقدار minSdkVersion را روی 21 و یا بیشتر قرار دهید.

دیتابیس فلاتر
آموزش برنامه نویسی فلاتر

ایجاد موجودیت های دیتابیس – Entity

هر کلاسی که قصد ذخیره سازی آن در دیتابیس را داشته باشیم یک موجودیت یا Entity به حساب می آید و به با @Entity مشخص می شود.

در این پروژه با دو نوع موجودیت روبرو هستیم:

Expense: موجودیت هزینه شامل مقدار هزینه, تاریخ ایجاد, متن

ExpenseType: موجودیت نوع هزینه شامل نام و شناسه

import 'package:objectbox/objectbox.dart';

/// {@template expense_type}
/// Expense type entity.
/// {@endtemplate}
@Entity()
class ExpenseType {
  /// {@macro expense_type}
  ExpenseType({
    this.id = 0,
    required this.identifier,
    required this.name,
  });

  /// Id of the entity, managed by the Objectbox.
  int id;

  /// Identifier for the [ExpenseType], generally an emoji.
  String identifier;

  /// Name for the [ExpenseType], must be unique.
  @Unique()
  String name;
}


/// {@template expense}
/// Expense entity.
/// {@endtemplate}
@Entity()
class Expense {
  /// {@macro expense}
  Expense({
    this.id = 0,
    required this.amount,
    this.note = '',
    required this.date,
  });

  /// Id of the expense entity, managed by Objectbox.
  int id;

  /// Amount that has been expensed.
  double amount;

  /// Optional note for the expense.
  String note;

  /// Date and time when the expense has been noted.
  @Property(type: PropertyType.dateNano)
  DateTime date;
}

بر اساس نیازهای پروژه میتوانید موجودیت های مختلفی برای پایگاه داده تعریف کنید.

ObjectBox شامل حاشیه نگاری های بیشتری میباشد که برای شخصی سازی بهتر کلاس ها میتوانید از آنها استفاده کنید مثل @Transient@Index@NameInDb@Unique@Property,و…

بعد از تکمیل این بخش نوبت به اجرای build_runner میباشد تا فایل های مورد نیاز برای ادامه کار ایجاد شوند.

flutter pub run build_runner watch — delete-conflicting-outputs

دو فایل جدید به نام های objectbox.g.dart و objectbox-model.json در بخش روت پروژه قرار گرفتند.

در مرحله بعدی روابط بین دو کلاس موجودیت را تعیین میکنیم. دیتابیس ObjectBox از انواع رابطه ها پشتیبانی میکند.

برای تعیین نوع روابط میدانیم که هر نوع هزینه میتوانید به چندین نمونه از کلاس هزینه بسط داده شود و هر نوع هزینه هم فقط مربوط به یک نوع هزینه می باشد. بنابراین طبق این توضیحات کدهای زیر را مینویسیم.

import 'package:objectbox/objectbox.dart';

/// {@template expense_type}
/// Expense type entity.
/// {@endtemplate}
@Entity()
class ExpenseType {
  // ...

  /// All the expenses linked to this [ExpenseType]
  @Backlink()
  final expenses = ToMany<Expense>();
}

/// {@template expense}
/// Expense entity.
/// {@endtemplate}
@Entity()
class Expense {
  // ...

  /// [ExpenseType] of the expense.
  final expenseType = ToOne<ExpenseType>();
}

طراحی یک Store

یک Store نقطه ورودی پایگاه داده ObjectBox در فلاتر میباشد. برای دسترسی به اطلاعات و یا انجام هر عملیاتی ما به این رابط نیاز داریم.

برای تمیز نگهداشتن کدهای نوشته شده یک فایل جدیدی به نام store_repository ایجاد میکنیم که مسئول پیاده سازی و مقداردهی store میباشد. بهتر از در برنامه همیشه یک نمونه از store وجود داشته باشد و باقی کلاس ها از آن استفاده کنند.

نام کلاس مورد نظر را StoreRepository میگذاریم.

import 'package:objectbox/objectbox.dart' as objectbox;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart' as path_provider;

import 'package:flutter_objectbox_example/objectbox.g.dart';

/// Repository to initilialize the ObjectBox Store object
class StoreRepository {
  late final objectbox.Store _store;

  /// Initializes the ObjectBox Store object.
  Future<void> initStore() async {
    final appDocumentsDirextory =
        await path_provider.getApplicationDocumentsDirectory();

    _store = Store(
      getObjectBoxModel(),
      directory: path.join(appDocumentsDirextory.path, 'expense-db'),
    );

    return;
  }

  /// Getter for the store object.
  objectbox.Store get store => _store;
}

ساخت ExpenseTypeRepository  و ExpenseRepository

کلاسی جدیدی به نام ExpenseTypeRepository ایجاد میکنیم که وظیفه ایجاد و حذف نوع هزینه ها در دیتابیس می باشد.

مشابه همین کلاس را با کاربردی مشابه به نام ExpenseRepository طراحی میکنیم که وظیفه آن ثبت, حذف و دریافت لیست هزینه ها می باشد.

import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:objectbox/objectbox.dart';

/// Repository to perform different operations with [ExpenseType].
class ExpenseTypeRepository {
  ExpenseTypeRepository({required this.store});
  final Store store;

  /// Persists the given [ExpenseType] in the store.
  void addExpenseType(ExpenseType expenseType) {
    store.box<ExpenseType>().put(expenseType);
  }

  /// Returns all stored [ExpenseType]s in this Box.
  List<ExpenseType> getAllExpenseTypes() {
    return store.box<ExpenseType>().getAll();
  }

  /// Deletes the [ExpenseType] with the given [id].
  bool deleteExpenseType(int id) {
    return store.box<ExpenseType>().remove(id);
  }
}
import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:objectbox/objectbox.dart';

/// Repository to perform different operations with [Expense].
class ExpenseRepository {
  ExpenseRepository({required this.store});
  final Store store;

  /// Persists the given [Expense] in the store.
  void addExpense(Expense expense, ExpenseType expenseType) {
    expense.expenseType.target = expenseType;
    store.box<Expense>().put(expense);
  }

  /// Deletes the [Expense] with the given [id].
  void removeExpense(int id) {
    store.box<Expense>().remove(id);
  }
}

دیتابیس ObjectBox  این امکان را در اختیار برنامه نویسان فلاتر قرار میدهد تا جریان اطلاعات را بی درنگ مورد بازبینی قرار دهند.

این کار به راحتی با استفاده از متد watch() قابل انجام می باشد. این متد یک نمونه جریان Stream<Query<T>>

باز میگرداند.

برای اینکه به محض ثبت شدن اطلاعات جدید در پایگاه داده یک رویداد دریافت کنیم ویژگی triggerImmediately را برابر true قرار میدهیم.

import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:objectbox/objectbox.dart';

/// Repository to perform different operations with [ExpenseType].
class ExpenseTypeRepository {
  ExpenseTypeRepository({required this.store});
  final Store store;
  
  // ...
  
  /// Provides a [Stream] of all expense types.
  Stream<List<ExpenseType>> getAllExpenseTypeStream() {
    final query = store.box<ExpenseType>().query();
    return query
        .watch(triggerImmediately: true)
        .map<List<ExpenseType>>((query) => query.find());
  }
}

برای دریافت اطلاعات براساس شروط مختلف از متد order() استفاده میکنیم.

import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:flutter_objectbox_example/objectbox.g.dart' as objectbox_g;
import 'package:objectbox/objectbox.dart';

/// Repository to perform different operations with [Expense].
class ExpenseRepository {
  ExpenseRepository({required this.store});
  final Store store;
  
  // ...
  
  /// Provides a [Stream] of all expenses.
  Stream<List<Expense>> getAllExpenseStream() {
    final query = store.box<Expense>().query();
    return query
        .watch(triggerImmediately: true)
        .map<List<Expense>>((query) => query.find());
  }

  /// Provides a [Stream] of total expense in last 7 days.
  Stream<double> expenseInLast7Days() {
    final query = store
        .box<Expense>()
        .query(objectbox_g.Expense_.date.greaterThan(
          DateTime.now()
                  .subtract(const Duration(days: 7))
                  .microsecondsSinceEpoch *
              1000,
        ))
        .watch(triggerImmediately: true);
    return query.map<double>((query) => query
        .find()
        .map<double>((e) => e.amount)
        .reduce((value, element) => value + element));
  }

  /// Provides a [Stream] of all expenses ordered by time.
  Stream<List<Expense>> getExpenseSortByTime() {
    final query = store.box<Expense>().query()
      ..order(objectbox_g.Expense_.date, flags: Order.descending);
    return query
        .watch(triggerImmediately: true)
        .map<List<Expense>>((query) => query.find());
  }

  /// Provides a [Stream] of all expenses ordered by amount.
  Stream<List<Expense>> getExpenseSortByAmount() {
    final query = store.box<Expense>().query()
      ..order(objectbox_g.Expense_.amount, flags: Order.descending);
    return query
        .watch(triggerImmediately: true)
        .map<List<Expense>>((query) => query.find());
  }
}

ایجاد کلاس RepositoryProvider 

تا این بخش کلاس های Repository مورد نیاز را به شکل کامل پیاده سازی کردیم. برای اینکه از این کلاس ها داخل اپلیکیشن استفاده کنیم و فقط یک نمونه از آنها داشته باشیم از کلاس دیگری به نام RepositoryProvider کمک خواهیم گرفت.

طبق کدهای زیر فایل main.dart را تغییر میدهیم. همچنین برای مقداردهی ObjectBox تعدادی دستور را برای تنظیمات برنامه به شکل سراسری تعریف میکنیم و متد runApp را با استفاده از یک نمونه از کلاس App فراخوانی میکنیم.

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_objectbox_example/app/app.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Bloc.observer = AppBlocObserver();

  StoreRepository storeRepository = StoreRepository();
  await storeRepository.initStore();

  runApp(App(storeRepository: storeRepository));
}

در فایل app.dart با استفاده از کلاس RepositoryProvider نمونه مورد نیاز از کلاس StoreRepository را در اپلیکیشن فراهم میکنیم. برای باقی کلاس های Repository هم نیز همین عمل را انجام میدهد.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_objectbox_example/expense/expense.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';
import 'package:flutter_objectbox_example/theme.dart';

class App extends StatelessWidget {
  const App({super.key, required StoreRepository storeRepository})
      : _storeRepository = storeRepository;
  final StoreRepository _storeRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider<StoreRepository>.value(
      value: _storeRepository,
      child: MultiRepositoryProvider(
        providers: [
          RepositoryProvider<ExpenseRepository>(
            create: (_) => ExpenseRepository(store: _storeRepository.store),
          ),
          RepositoryProvider<ExpenseTypeRepository>(
            create: (_) => ExpenseTypeRepository(store: _storeRepository.store),
          ),
        ],
        child: MaterialApp(
          title: 'Flutter Objectbox Example',
          debugShowCheckedModeBanner: false,
          theme: createAppTheme(context),
          home: const ExpensePage(),
        ),
      ),
    );
  }
}

اعتبارسنجی فرم ها

طبق اسکرین شات های پروژه نیاز به طراحی فرم هایی داریم که کاربر نوع هزینه ها و همچنین انواع هزینه ها داخل اپلیکیشن بتواند ثبت کند.

قبل از طراحی بخش رابط کاربری فلاتر نیاز داریم تا اعتبارسنجی اطلاعات وارد شده را پیاده سازی کنیم و از ذخیره اطلاعات نادرست در پایگاه داده جلوگیری کنیم.

به همین منظور کلاس های مختلفی برای اعتبارسنجی نام, مقدار, نوع هزینه و…. ایجاد میکنیم.

برای ساخت این کلاس ها از پکیج Formz استفاده میکنیم.

import 'package:formz/formz.dart';

enum IdentifierValidationError { invalid }

class Identifier extends FormzInput<String, IdentifierValidationError> {
  const Identifier.pure() : super.pure('');
  const Identifier.dirty([String identifier = '']) : super.dirty(identifier);

  static final RegExp _identifierRegExp = RegExp(r'^.{1,5}$');

  @override
  IdentifierValidationError? validator(String value) {
    return _identifierRegExp.hasMatch(value)
        ? null
        : IdentifierValidationError.invalid;
  }
}
import 'package:formz/formz.dart';

enum NameValidationError { invalid }

class Name extends FormzInput<String, NameValidationError> {
  const Name.pure() : super.pure('');
  const Name.dirty([String name = '']) : super.dirty(name);

  static final RegExp _nameRegExp = RegExp(r'^.{1,20}$');

  @override
  NameValidationError? validator(String value) {
    return _nameRegExp.hasMatch(value) ? null : NameValidationError.invalid;
  }
}
import 'package:formz/formz.dart';

enum AmountValidationError { invalid }

class Amount extends FormzInput<String, AmountValidationError> {
  const Amount.pure() : super.pure('');
  const Amount.dirty([String amount = '']) : super.dirty(amount);

  static final RegExp _amountRegExp = RegExp(r'^\d+(\.\d{1,2})?$');

  @override
  AmountValidationError? validator(String value) {
    return _amountRegExp.hasMatch(value) ? null : AmountValidationError.invalid;
  }
}
import 'package:formz/formz.dart';

enum NoteValidationError { invalid }

class Note extends FormzInput<String, NoteValidationError> {
  const Note.pure() : super.pure('');
  const Note.dirty([String note = '']) : super.dirty(note);

  static final RegExp _noteRegExp = RegExp(r'^.{1,20}$');

  @override
  NoteValidationError? validator(String value) {
    return _noteRegExp.hasMatch(value) ? null : NoteValidationError.invalid;
  }
}

پیاده سازی الگوی Bloc

در این پروژه دو بلاک جداگانه برای ثبت هزینه ها و نوع هزینه طراحی میکنیم.

برای شروع ابتدا با کلاس ExpenseTypeFormBloc کار خودمون و آغاز میکنیم.

این کلاس مسئول مدیریت State فرم ثبت نوع هزینه میباشد که در واقع کلاس مرتبط با آن را ExpenseTypeFormState نامگذاری میکنیم.

part of 'expense_type_form_bloc.dart';

class ExpenseTypeFormState extends Equatable {
  const ExpenseTypeFormState({
    this.identifier = const Identifier.pure(),
    this.name = const Name.pure(),
    this.status = FormzStatus.pure,
    this.errorMessage,
  });

  final Identifier identifier;
  final Name name;
  final FormzStatus status;
  final String? errorMessage;

  @override
  List<Object> get props => [identifier, name, status];

  ExpenseTypeFormState copyWith({
    Identifier? identifier,
    Name? name,
    FormzStatus? status,
    String? errorMessage,
  }) {
    return ExpenseTypeFormState(
      identifier: identifier ?? this.identifier,
      name: name ?? this.name,
      status: status ?? this.status,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}

میدانیم که الگوی بلاک شامل کلاس دیگری برای Eventها نیز میباشد. بنابراین کلاس مرتبط با این بخش را نیز ایجاد میکنیم.

part of 'expense_type_form_bloc.dart';

abstract class ExpenseTypeFormEvent extends Equatable {
  const ExpenseTypeFormEvent();

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

class IdentifierChanged extends ExpenseTypeFormEvent {
  const IdentifierChanged(this.value);
  final String value;

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

class NameChanged extends ExpenseTypeFormEvent {
  const NameChanged(this.value);
  final String value;

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

class ExpenseTypeFormSubmitted extends ExpenseTypeFormEvent {}

در نهایت نوبت به کدهای کلاس اصلی یعنی بلاک میرسد.

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:flutter_objectbox_example/form_inputs/form_inputs.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';
import 'package:formz/formz.dart';

part 'expense_type_form_event.dart';
part 'expense_type_form_state.dart';

class ExpenseTypeFormBloc
    extends Bloc<ExpenseTypeFormEvent, ExpenseTypeFormState> {
  ExpenseTypeFormBloc({
    required ExpenseTypeRepository expenseTypeRepository,
  })  : _expenseTypeRepository = expenseTypeRepository,
        super(const ExpenseTypeFormState()) {
    on<IdentifierChanged>(_onIdentifierChanged);
    on<NameChanged>(_onNameChanged);
    on<ExpenseTypeFormSubmitted>(_onExpenseTypeFormSubmitted);
  }

  final ExpenseTypeRepository _expenseTypeRepository;

  void _onIdentifierChanged(
    IdentifierChanged event,
    Emitter<ExpenseTypeFormState> emit,
  ) {
    final identifier = Identifier.dirty(event.value);
    emit(state.copyWith(
      identifier: identifier,
      status: Formz.validate([identifier, state.name]),
    ));
  }

  void _onNameChanged(
    NameChanged event,
    Emitter<ExpenseTypeFormState> emit,
  ) {
    final name = Name.dirty(event.value);
    emit(state.copyWith(
      name: name,
      status: Formz.validate([state.identifier, name]),
    ));
  }

  void _onExpenseTypeFormSubmitted(
    ExpenseTypeFormSubmitted event,
    Emitter<ExpenseTypeFormState> emit,
  ) {
    if (!state.status.isValidated) return;
    emit(state.copyWith(status: FormzStatus.submissionInProgress));

    try {
      _expenseTypeRepository.addExpenseType(
        ExpenseType(identifier: state.identifier.value, name: state.name.value),
      );
      emit(state.copyWith(status: FormzStatus.submissionSuccess));
    } catch (e) {
      emit(state.copyWith(
        status: FormzStatus.submissionFailure,
        errorMessage: 'You may have entered a duplicate expense type',
      ));
    }
  }
}

بعد از تکمیل این بخش برای مدیریت کردن عملیات درج, آپدیت و حذف هزینه از دیتابیس به کلاس ExpenseFormBloc نیاز داریم.

تنها تفاوت این بخش با بخش قبلی در این است که نیاز به یک رویداد اضافی برای دریافت تمام نوع هزینه ها داریم.

part of 'expense_form_bloc.dart';

class ExpenseFormState extends Equatable {
  const ExpenseFormState({
    this.amount = const Amount.pure(),
    this.note = const Note.pure(),
    this.expenseTypes = const [],
    this.selectedTypeIndex = 0,
    this.status = FormzStatus.pure,
    this.errorMessage,
  });

  final Amount amount;
  final Note note;
  final List<ExpenseType> expenseTypes;
  final int selectedTypeIndex;
  final FormzStatus status;
  final String? errorMessage;

  @override
  List<Object> get props =>
      [amount, note, expenseTypes, selectedTypeIndex, status];

  ExpenseFormState copyWith({
    Amount? amount,
    Note? note,
    List<ExpenseType>? expenseTypes,
    int? selectedTypeIndex,
    FormzStatus? status,
    String? errorMessage,
  }) {
    return ExpenseFormState(
      amount: amount ?? this.amount,
      note: note ?? this.note,
      expenseTypes: expenseTypes ?? this.expenseTypes,
      selectedTypeIndex: selectedTypeIndex ?? this.selectedTypeIndex,
      status: status ?? this.status,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}
part of 'expense_form_bloc.dart';

abstract class ExpenseFormEvent extends Equatable {
  const ExpenseFormEvent();

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

class InitExpenseForm extends ExpenseFormEvent {}

class AmountChanged extends ExpenseFormEvent {
  const AmountChanged(this.value);
  final String value;

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

class NoteChanged extends ExpenseFormEvent {
  const NoteChanged(this.value);
  final String value;

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

class ExpenseTypeChanged extends ExpenseFormEvent {
  const ExpenseTypeChanged(this.selectedIndex);
  final int selectedIndex;

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

class ExpenseFormSubmitted extends ExpenseFormEvent {}
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:flutter_objectbox_example/form_inputs/form_inputs.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';
import 'package:formz/formz.dart';

part 'expense_form_event.dart';
part 'expense_form_state.dart';

class ExpenseFormBloc extends Bloc<ExpenseFormEvent, ExpenseFormState> {
  ExpenseFormBloc({
    required ExpenseRepository expenseRepository,
    required ExpenseTypeRepository expenseTypeRepository,
  })  : _expenseRepository = expenseRepository,
        _expenseTypeRepository = expenseTypeRepository,
        super(const ExpenseFormState()) {
    on<InitExpenseForm>(_onInitExpenseForm);
    on<AmountChanged>(_onAmountChanged);
    on<NoteChanged>(_onNoteChanged);
    on<ExpenseTypeChanged>(_onExpenseTypeChanged);
    on<ExpenseFormSubmitted>(_onExpenseFormSubmitted);
  }

  final ExpenseRepository _expenseRepository;
  final ExpenseTypeRepository _expenseTypeRepository;

  void _onInitExpenseForm(
    InitExpenseForm event,
    Emitter<ExpenseFormState> emit,
  ) {
    final expenseTypes = _expenseTypeRepository.getAllExpenseTypes();
    emit(state.copyWith(expenseTypes: expenseTypes));
  }

  void _onAmountChanged(
    AmountChanged event,
    Emitter<ExpenseFormState> emit,
  ) {
    final amount = Amount.dirty(event.value);
    emit(state.copyWith(
      amount: amount,
      status: Formz.validate([amount, state.note]),
    ));
  }

  void _onNoteChanged(
    NoteChanged event,
    Emitter<ExpenseFormState> emit,
  ) {
    final note = Note.dirty(event.value);
    emit(state.copyWith(
      note: note,
      status: Formz.validate([state.amount, note]),
    ));
  }

  void _onExpenseTypeChanged(
    ExpenseTypeChanged event,
    Emitter<ExpenseFormState> emit,
  ) {
    emit(state.copyWith(
      selectedTypeIndex: event.selectedIndex,
      status: Formz.validate([state.amount, state.note]),
    ));
  }

  void _onExpenseFormSubmitted(
    ExpenseFormSubmitted event,
    Emitter<ExpenseFormState> emit,
  ) {
    if (!state.status.isValidated) return;
    emit(state.copyWith(status: FormzStatus.submissionInProgress));

    final expenseType = state.expenseTypes.elementAt(state.selectedTypeIndex);
    try {
      _expenseRepository.addExpense(
        Expense(
          amount: double.parse(state.amount.value),
          note: state.note.value,
          date: DateTime.now(),
        ),
        expenseType,
      );
      emit(state.copyWith(status: FormzStatus.submissionSuccess));
    } catch (e) {
      emit(state.copyWith(
        status: FormzStatus.submissionFailure,
        errorMessage: e.toString(),
      ));
    }
  }
}

کلاس های بلاکی که تا به اینجا طراحی کردیم تنها مسئول دریافت و اعتبارسنجی اطلاعات از فرم ها هستند و هیچ عملیاتی بروی دیتابیس ObjectBox انجام نمیدهند.

بنابراین برای شروع نیاز به رویدادهای جدیدی داریم تا بتوانیم این عملیات های مورد نظر را انجام دهیم.

برای این بخش ابتدا به سراغ کلاس ExpenseTypeEvent میرویم.

رویداد ExpenseTypeDeleted برای حذف اطلاعات ذخیره شده استفاده میشود.

رویداد ExpenseTypeSubscriptionRequested هم به عنوان نقطه شروع رویدادها میباشد که کلاس بلاک را به جریان (Stream) کلاس ExpenseTypeRepository متصل میکند.

part of 'expense_type_bloc.dart';

abstract class ExpenseTypeEvent extends Equatable {
  const ExpenseTypeEvent();

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

class ExpenseTypeSubscriptionRequested extends ExpenseTypeEvent {}

class ExpenseTypeDeleted extends ExpenseTypeEvent {
  const ExpenseTypeDeleted(this.id);
  final int id;

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

کلاس ExpenseTypeState هم لیست انواع هزینه و وضعیت را در خود نگهداری میکند.

part of 'expense_type_bloc.dart';

enum ExpenseTypeStatus { initial, loading, success, failure }

class ExpenseTypeState extends Equatable {
  const ExpenseTypeState({
    this.status = ExpenseTypeStatus.initial,
    this.expenseTypes = const [],
  });

  final ExpenseTypeStatus status;
  final List<ExpenseType> expenseTypes;

  @override
  List<Object> get props => [status, expenseTypes];

  ExpenseTypeState copyWith({
    ExpenseTypeStatus? status,
    List<ExpenseType>? expenseTypes,
  }) {
    return ExpenseTypeState(
      status: status ?? this.status,
      expenseTypes: expenseTypes ?? this.expenseTypes,
    );
  }
}

زمانی که رویداد ExpenseTypeSubscriptionRequested اجرا میشود کلاس بلاک یک state جدیدی را فراخوانی میکند و در این زمان رابط کاربری شامل یک ویجت لودینگ میباشد.

زمانی هم که ExpenseTypeDeleted اجرا میشود به راحتی متد مرتباط با حذف اطلاعات را از کلاس repository فراخوانی خواهیم کرد.

در متد _onExpenseTypeDeleted هیچوقت یک رویداد را emit نمیکنیم بجای آن کلاس repository را از این عملیات با خبر میکنیم میکنیم و یک لیست جدید بروزرسانی شده از نوع هزینه ها را دریافت میکنیم.

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';

part 'expense_type_event.dart';
part 'expense_type_state.dart';

class ExpenseTypeBloc extends Bloc<ExpenseTypeEvent, ExpenseTypeState> {
  ExpenseTypeBloc({
    required ExpenseTypeRepository expenseTypeRepository,
  })  : _expenseTypeRepository = expenseTypeRepository,
        super(const ExpenseTypeState()) {
    on<ExpenseTypeSubscriptionRequested>(_onSubscriptionRequested);
    on<ExpenseTypeDeleted>(_onExpenseTypeDeleted);
  }

  final ExpenseTypeRepository _expenseTypeRepository;

  Future<void> _onSubscriptionRequested(
    ExpenseTypeSubscriptionRequested event,
    Emitter<ExpenseTypeState> emit,
  ) async {
    emit(state.copyWith(status: ExpenseTypeStatus.loading));
    await emit.forEach<List<ExpenseType>>(
      _expenseTypeRepository.getAllExpenseTypeStream(),
      onData: (expenseTypes) => state.copyWith(
        status: ExpenseTypeStatus.success,
        expenseTypes: expenseTypes,
      ),
      onError: (_, __) => state.copyWith(
        status: ExpenseTypeStatus.failure,
      ),
    );
  }

  void _onExpenseTypeDeleted(
    ExpenseTypeDeleted event,
    Emitter<ExpenseTypeState> emit,
  ) {
    _expenseTypeRepository.deleteExpenseType(event.id);
  }
}

طراحی صفحه ExpenseType

کلاس ExpenseTypePage مسئول ایجاد و فراهم کردن نمونه از ExpenseTypeFormBloc و ExpenseTypeBloc برای کلاس ExpenseTypeView میباشد.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_objectbox_example/expense_type/expense_type.dart';
import 'package:flutter_objectbox_example/form_inputs/form_inputs.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';

class ExpenseTypePage extends StatelessWidget {
  const ExpenseTypePage({super.key});

  static Route<void> route() => MaterialPageRoute<void>(
        builder: (_) => const ExpenseTypePage(),
        fullscreenDialog: true,
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('EXPENSE TYPES')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: MultiBlocProvider(
          providers: [
            BlocProvider<ExpenseTypeFormBloc>(
              create: (context) => ExpenseTypeFormBloc(
                expenseTypeRepository: context.read<ExpenseTypeRepository>(),
              ),
            ),
            BlocProvider<ExpenseTypeBloc>(
              create: (context) => ExpenseTypeBloc(
                expenseTypeRepository: context.read<ExpenseTypeRepository>(),
              )..add(ExpenseTypeSubscriptionRequested()),
            ),
          ],
          child: const ExpenseTypeView(),
        ),
      ),
    );
  }
}

کلاس ExpenseTypeView هم مسئول طراحی رابط کاربری و نمایش لیست انواع هزینه ها می باشد.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_objectbox_example/expense_type/expense_type.dart';
import 'package:flutter_objectbox_example/form_inputs/form_inputs.dart';
import 'package:formz/formz.dart';

class ExpenseTypeView extends StatelessWidget {
  const ExpenseTypeView({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        BlocListener<ExpenseTypeFormBloc, ExpenseTypeFormState>(
          listenWhen: (previous, current) => previous.status != current.status,
          listener: (context, state) {
            if (state.status.isSubmissionFailure) {
              ScaffoldMessenger.of(context)
                ..hideCurrentSnackBar()
                ..showSnackBar(
                  SnackBar(
                    content: Text(state.errorMessage ?? 'Something went wrong!'),
                  ),
                );
            }
          },
        ),
        BlocListener<ExpenseTypeBloc, ExpenseTypeState>(
          listenWhen: (previous, current) => previous.status != current.status,
          listener: (context, state) {
            if (state.status == ExpenseTypeStatus.failure) {
              ScaffoldMessenger.of(context)
                ..hideCurrentSnackBar()
                ..showSnackBar(
                  const SnackBar(
                    content: Text('Something went wrong while loading types!'),
                  ),
                );
            }
          },
        ),
      ],
      child: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text('Add an identifier', style: TextStyle(fontSize: 18)),
            _IdentifierTextField(),
            const SizedBox(height: 16.0),
            const Text('Add a name', style: TextStyle(fontSize: 18)),
            _NameTextField(),
            const SizedBox(height: 16.0),
            _SubmitButton(),
            const SizedBox(height: 16.0),
            const Divider(),
            const SizedBox(height: 16.0),
            const Text('AVAILABLE EXPENSE TYPES', style: TextStyle(fontSize: 18)),
            const SizedBox(height: 24.0),
            _ExpenseTypesList(),
          ],
        ),
      ),
    );
  }
}

class _IdentifierTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseTypeFormBloc, ExpenseTypeFormState>(
      buildWhen: (previous, current) =>
          previous.identifier != current.identifier,
      builder: (context, state) {
        return TextField(
          key: const Key('expenseTypeForm_identifier_textField'),
          onChanged: (value) =>
              context.read<ExpenseTypeFormBloc>().add(IdentifierChanged(value)),
          decoration: InputDecoration(
            hintText: 'Identifier',
            errorText: state.identifier.invalid ? 'Must be within 5 characters' : null,
          ),
        );
      },
    );
  }
}

class _NameTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseTypeFormBloc, ExpenseTypeFormState>(
      buildWhen: (previous, current) => previous.name != current.name,
      builder: (context, state) {
        return TextField(
          key: const Key('expenseTypeForm_name_textField'),
          onChanged: (value) =>
              context.read<ExpenseTypeFormBloc>().add(NameChanged(value)),
          decoration: InputDecoration(
            hintText: 'Name',
            errorText: state.name.invalid ? 'Must be within 10 characters' : null,
          ),
        );
      },
    );
  }
}

class _SubmitButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseTypeFormBloc, ExpenseTypeFormState>(
      buildWhen: (previous, current) => previous.status != current.status,
      builder: (context, state) {
        return ElevatedButton(
          onPressed: state.status.isValidated
              ? () => context.read<ExpenseTypeFormBloc>().add(ExpenseTypeFormSubmitted())
              : null,
          child: state.status.isSubmissionInProgress
              ? const CircularProgressIndicator()
              : const Text('SAVE', style: TextStyle(fontSize: 18)),
        );
      },
    );
  }
}

class _ExpenseTypesList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseTypeBloc, ExpenseTypeState>(
      builder: (context, state) {
        if (state.expenseTypes.isEmpty) {
          if (state.status == ExpenseTypeStatus.loading) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          } else if (state.status != ExpenseTypeStatus.success) {
            return const SizedBox();
          } else {
            return const EmptyContent();
          }
        }
        return GridView.builder(
          shrinkWrap: true,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 4,
          ),
          itemCount: state.expenseTypes.length,
          itemBuilder: (context, index) {
            final expenseType = state.expenseTypes.elementAt(index);
            return ExpenseTypeCard(expenseType: expenseType);
          },
        );
      },
    );
  }
}

طراحی ExpenseBloc  – الگوی بلاک فلاتر

این بخش تا حدود بسیار زیادی مثل بخش قبلی میباشد. رویدادی به نام ExpenseListRequested داریم که رویداد اولیه میباشد که مسئول اتصال بلاک به ExpenseRepository است.

رویدادهای دیگر مثل ExpenseListSortByTimeRequested, ExpenseListSortByAmountRequested, ExpenseInLast7DaysRequested به متدهای متناظر در کلاس ExpenseRepository مرتبط میشوند.

part of 'expense_bloc.dart';

abstract class ExpenseEvent extends Equatable {
  const ExpenseEvent();

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

class ExpenseListRequested extends ExpenseEvent {}

class ExpenseListSortByTimeRequested extends ExpenseEvent {}

class ExpenseListSortByAmountRequested extends ExpenseEvent {}

class ToggleExpenseSort extends ExpenseEvent {}

class ExpenseInLast7DaysRequested extends ExpenseEvent {}

class ExpenseDeleted extends ExpenseEvent {
  const ExpenseDeleted(this.id);
  final int id;

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

کلاس ExpenseState اطلاعات مختلفی شامل وضعیت, لیست هزینه, هزینه های 7 روز گذشته و… را در خود نگهداری میکند.

part of 'expense_bloc.dart';

enum ExpenseStatus { initial, loading, success, failure }

enum ExpenseSort { none, time, amount }

class ExpenseState extends Equatable {
  const ExpenseState({
    this.status = ExpenseStatus.initial,
    this.expenses = const [],
    this.expenseSort = ExpenseSort.none,
    this.expenseInLast7Days = 0.0,
  });

  final ExpenseStatus status;
  final List<Expense> expenses;
  final ExpenseSort expenseSort;
  final double expenseInLast7Days;

  @override
  List<Object> get props => [status, expenses, expenseSort, expenseInLast7Days];

  ExpenseState copyWith({
    ExpenseStatus? status,
    List<Expense>? expenses,
    ExpenseSort? expenseSort,
    double? expenseInLast7Days,
  }) {
    return ExpenseState(
      status: status ?? this.status,
      expenses: expenses ?? this.expenses,
      expenseSort: expenseSort ?? this.expenseSort,
      expenseInLast7Days: expenseInLast7Days ?? this.expenseInLast7Days,
    );
  }
}

بعد از اتمام کارها نیاز به ساخت کلاس بلاک داریم که از نظر منطقی کاملا شبیه به کلاس ExpenseTypeBloc میباشد.

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_objectbox_example/entities/entities.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';

part 'expense_event.dart';
part 'expense_state.dart';

class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
  ExpenseBloc({
    required ExpenseRepository expenseRepository,
  })  : _expenseRepository = expenseRepository,
        super(const ExpenseState()) {
    on<ExpenseListRequested>(_onExpenseList);
    on<ExpenseListSortByTimeRequested>(_onExpenseListSortByTime);
    on<ExpenseListSortByAmountRequested>(_onExpenseListSortByAmount);
    on<ToggleExpenseSort>(_onToggleExpenseSort);
    on<ExpenseInLast7DaysRequested>(_onExpenseInLast7DaysRequested);
    on<ExpenseDeleted>(_onExpenseDeleted);
  }

  final ExpenseRepository _expenseRepository;

  Future<void> _onExpenseList(
    ExpenseListRequested event,
    Emitter<ExpenseState> emit
  ) async {
    emit(state.copyWith(status: ExpenseStatus.loading));
    await emit.forEach<List<Expense>>(
      _expenseRepository.getAllExpenseStream(),
      onData: (expenses) => state.copyWith(
        status: ExpenseStatus.success,
        expenses: expenses,
      ),
      onError: (_, __) => state.copyWith(
        status: ExpenseStatus.failure,
      ),
    );
  }

  Future<void> _onExpenseListSortByTime(
    ExpenseListSortByTimeRequested event,
    Emitter<ExpenseState> emit,
  ) async {
    emit(state.copyWith(status: ExpenseStatus.loading));
    await emit.forEach<List<Expense>>(
      _expenseRepository.getExpenseSortByTime(),
      onData: (expenses) => state.copyWith(
        status: ExpenseStatus.success,
        expenses: expenses,
      ),
      onError: (_, __) => state.copyWith(
        status: ExpenseStatus.failure,
      ),
    );
  }

  Future<void> _onExpenseListSortByAmount(
    ExpenseListSortByAmountRequested event,
    Emitter<ExpenseState> emit,
  ) async {
    emit(state.copyWith(status: ExpenseStatus.loading));
    await emit.forEach<List<Expense>>(
      _expenseRepository.getExpenseSortByAmount(),
      onData: (expenses) => state.copyWith(
        status: ExpenseStatus.success,
        expenses: expenses,
      ),
      onError: (_, __) => state.copyWith(
        status: ExpenseStatus.failure,
      ),
    );
  }

  void _onToggleExpenseSort(
    ToggleExpenseSort event,
    Emitter<ExpenseState> emit
  ) {
    if (state.expenseSort == ExpenseSort.none) {
      emit(state.copyWith(expenseSort: ExpenseSort.time));
      add(ExpenseListSortByTimeRequested());
    } else if (state.expenseSort == ExpenseSort.time) {
      emit(state.copyWith(expenseSort: ExpenseSort.amount));
      add(ExpenseListSortByAmountRequested());
    } else if (state.expenseSort == ExpenseSort.amount) {
      emit(state.copyWith(expenseSort: ExpenseSort.none));
      add(ExpenseListRequested());
    }
  }

  Future<void> _onExpenseInLast7DaysRequested(
    ExpenseInLast7DaysRequested event,
    Emitter<ExpenseState> emit
  ) async {
    await emit.forEach<double>(
      _expenseRepository.expenseInLast7Days(),
      onData: (totalExpenses) => state.copyWith(
        expenseInLast7Days: totalExpenses,
      ),
      onError: (_, __) => state.copyWith(
        expenseInLast7Days: 0.0,
      ),
    );
  }

  void _onExpenseDeleted(ExpenseDeleted event, Emitter<ExpenseState> emit) {
    _expenseRepository.removeExpense(event.id);
  }
}

ویجت ExpensePage هم نیز مسئول ساخت و فراهم کردن یک نمونه از کلاس ExpenseBloc و ارجاع آن به بخش رابط کاربری که کلاس ExpenseView نام دارد میباشد.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_objectbox_example/expense/expense.dart';
import 'package:flutter_objectbox_example/expense_type/expense_type.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';

class ExpensePage extends StatelessWidget {
  const ExpensePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Expense Tracker'),
        actions: [
          IconButton(
            onPressed: () => Navigator.push(context, ExpenseTypePage.route()),
            icon: const Icon(Icons.app_registration),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: BlocProvider<ExpenseBloc>(
          create: (_) =>
              ExpenseBloc(expenseRepository: context.read<ExpenseRepository>())
                ..add(ExpenseListRequested())
                ..add(ExpenseInLast7DaysRequested()),
          child: const ExpenseView(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(context, ExpenseFormPage.route()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

class ExpenseView extends StatelessWidget {
  const ExpenseView({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _LastWeekSpentWidget(),
        const SizedBox(height: 16.0),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Text('ALL EXPENSES', style: TextStyle(fontSize: 18)),
            _ExpenseSortToggleWidget(),
          ],
        ),
        const SizedBox(height: 24.0),
        Expanded(
          child: BlocBuilder<ExpenseBloc, ExpenseState>(
            buildWhen: (previous, current) =>
                previous.expenses != current.expenses,
            builder: (context, state) {
              return ListView.builder(
                itemCount: state.expenses.length,
                itemBuilder: (context, index) {
                  final expense = state.expenses.elementAt(index);
                  return ExpenseTile(expense: expense);
                },
              );
            },
          ),
        ),
      ],
    );
  }
}

class _LastWeekSpentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseBloc, ExpenseState>(
      buildWhen: (previous, current) =>
          previous.expenseInLast7Days != current.expenseInLast7Days,
      builder: (context, state) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Spent last 7 days', style: TextStyle(color: Colors.black45)),
            const SizedBox(height: 8),
            Text(
              '₹ ${state.expenseInLast7Days.toStringAsFixed(2)}',
              style: const TextStyle(fontSize: 40, fontWeight: FontWeight.w500),
            ),
          ],
        );
      },
    );
  }
}

class _ExpenseSortToggleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseBloc, ExpenseState>(
      buildWhen: (previous, current) => previous.expenseSort != current.expenseSort,
      builder: (context, state) {
        return Row(
          children: [
            const Text('Sort by', style: TextStyle(fontSize: 18)),
            TextButton(
              onPressed: () => context.read<ExpenseBloc>().add(ToggleExpenseSort()),
              child: Row(
                children: [
                  Text(state.expenseSort.name.toUpperCase()),
                  const SizedBox(width: 4),
                  const Icon(Icons.code, size: 18),
                ],
              ),
            ),
          ],
        );
      },
    );
  }
}

در انتها کدهای بخش کلاس ExpenseFormPage نیز به شکل زیر خواهد بود.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_objectbox_example/expense/expense.dart';
import 'package:flutter_objectbox_example/form_inputs/form_inputs.dart';
import 'package:flutter_objectbox_example/repository/repository.dart';
import 'package:formz/formz.dart';

class ExpenseFormPage extends StatelessWidget {
  const ExpenseFormPage({super.key});

  static Route route() => MaterialPageRoute(builder: (_) => const ExpenseFormPage());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('EXPENSE')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: BlocProvider<ExpenseFormBloc>(
          create: (context) => ExpenseFormBloc(
            expenseRepository: context.read<ExpenseRepository>(),
            expenseTypeRepository: context.read<ExpenseTypeRepository>(),
          )..add(InitExpenseForm()),
          child: const ExpenseFormView(),
        ),
      ),
    );
  }
}

class ExpenseFormView extends StatelessWidget {
  const ExpenseFormView({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocListener<ExpenseFormBloc, ExpenseFormState>(
      listenWhen: (previous, current) => previous.status != current.status,
      listener: (context, state) {
        if (state.status.isSubmissionFailure) {
          ScaffoldMessenger.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              SnackBar(content: Text(state.errorMessage ?? 'Something went wrong')),
            );
        } else if (state.status.isSubmissionSuccess) {
          Navigator.pop(context);
        }
      },
      child: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text('Add amount', style: TextStyle(fontSize: 18)),
            _AmountTextField(),
            const SizedBox(height: 16),
            const Text('Add a note', style: TextStyle(fontSize: 18)),
            _NoteTextField(),
            const SizedBox(height: 24),
            const Text('Select an expense type', style: TextStyle(fontSize: 18)),
            const SizedBox(height: 16),
            _ExpenseTypeSelectionWidget(),
            const SizedBox(height: 16),
            _AddExpenseButton(),
          ],
        ),
      ),
    );
  }
}

class _AmountTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseFormBloc, ExpenseFormState>(
      buildWhen: (previous, current) => previous.amount != current.amount,
      builder: (context, state) {
        return TextField(
          key: const Key('expenseForm_amount_textField'),
          onChanged: (value) =>
              context.read<ExpenseFormBloc>().add(AmountChanged(value)),
          keyboardType: TextInputType.number,
          decoration: InputDecoration(
            hintText: 'Amount',
            errorText: state.amount.invalid ? 'Must be a number' : null,
          ),
        );
      },
    );
  }
}

class _NoteTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseFormBloc, ExpenseFormState>(
      buildWhen: (previous, current) => previous.note != current.note,
      builder: (context, state) {
        return TextField(
          key: const Key('expenseForm_note_textField'),
          onChanged: (value) =>
              context.read<ExpenseFormBloc>().add(NoteChanged(value)),
          decoration: InputDecoration(
            hintText: 'Note',
            errorText: state.note.invalid ? 'Must be within 10 characters' : null,
          ),
        );
      },
    );
  }
}

class _ExpenseTypeSelectionWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseFormBloc, ExpenseFormState>(
      buildWhen: (previous, current) =>
          previous.selectedTypeIndex != current.selectedTypeIndex ||
          previous.expenseTypes != current.expenseTypes,
      builder: (context, state) {
        return GridView.builder(
          physics: const ScrollPhysics(),
          shrinkWrap: true,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 4,
          ),
          itemCount: state.expenseTypes.length,
          itemBuilder: (context, index) {
            final expenseType = state.expenseTypes.elementAt(index);
            return InkWell(
              onTap: () => context.read<ExpenseFormBloc>().add(ExpenseTypeChanged(index)),
              child: TypeSelectionCard(
                expenseType: expenseType,
                backgroundColor: index == state.selectedTypeIndex ? Colors.grey[300] : null,
              ),
            );
          },
        );
      },
    );
  }
}

class _AddExpenseButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ExpenseFormBloc, ExpenseFormState>(
      buildWhen: (previous, current) => previous.status != current.status,
      builder: (context, state) {
        return ElevatedButton(
          key: const Key('expenseForm_addExpense_elevatedButton'),
          onPressed: state.status.isValidated
              ? () => context.read<ExpenseFormBloc>().add(ExpenseFormSubmitted())
              : null,
          child: state.status.isSubmissionInProgress
              ? const CircularProgressIndicator()
              : const Text('SAVE', style: TextStyle(fontSize: 18)),
        );
      },
    );
  }
}

در این آموزش بخش بسیار زیادی از کدها مرتبط با معماری بلاک بود و اگر با آن آشنا نیستید شاید در این آموزش کمی دچار مشکل شوید.

پیشنهاد میکنم برای مشاهده مثال های ساده تر از داکیومنت اصلی ObjectBox نیز استفاده کنید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

Hesam
27 دسامبر 2022
آموزش فارسی فلاتر
آموزش فارسی flutter