1 year with Flutter in production

This is a text version of my presentation at DartUp 2020 (in English). In it, I share the problems we encountered, discuss our architectural approach, talk about useful libraries, and answer the question whether this idea was successful - to take and rewrite everything.





What are we doing?

Our main product is a hotel management system. Big and complex. There are also a few smaller products, one of which is a mobile application designed primarily for hotel staff. Initially, it was a native app for Android and iOS, but about a year and a half ago we decided to rewrite it in Flutter. And they rewrote it.





First, a few words about the application itself.





In general, this is the most common B2B application with everything you can expect from it: authorization, profile management, messages and tasks, forms and interaction with the backend.





, . -, UI, - ( Material Design Cupertino Design, ). , , . -, , .. , . , , .





. , .





API. DTO . , . . – , .





, – " ", – "", – " ". - ( / ).





– . -. , API. , , ( , – ), . , , - API DTO . , .





. Flutter. - , "" , .





BLoC

BLoC. , , : UI- ( , ) BLoC (Business Logic Component, -). BLoC – , ( UI, BLoC). BLoC , , , UI ( ) BLoC:





Redux (, ), : , store . BLoC', "-".





, – , , - , :





, - ( , , ) .





BLoC bloc. , , .





BLoC' ( ).





: BlocA



, BlocB



, BlocB



  BlocA



. , , BlocA



  BLoC'. BlocA



  Stream<StateB>



 ( Sink<EventB>



, - BlocB



). , BlocB



 ( Stream<StateB>



  Sink<EventB>



), BlocA



 , StateB



. , , Stream<StateB>



  BlocB



.





flutter_bloc



, : , BLoC ViewModel, UI-, , . , , UI UI. BLoC ( , -, ).





, – UI BLoC – : , - Flutter', GUI , CLI. , , UI-, BLoC' .






, .





, , , , . ( , Dart – ), , , .





: . , , , , .





– . ( , ).





, . , BLoC' (aka sealed classes – , ). – . - throw



. Either<E, R>



, , , . , , .





( , ), - , NNBD , - null



. , , - non-nullable, " " Optional<T>



.





. , , ; , .





freezed





-, freezed – , , - sealed Dart'.





- :





@freezed
abstract class TasksEvent with _$TasksEvent {
  const factory TasksEvent.fetchRequested() = FetchRequested;

  const factory TasksEvent.fetchCompleted(Either<Exception, TasksData> result) =
      FetchCompleted;

  const factory TasksEvent.filtersUpdated(TaskFilters filters) = FiltersUpdated;

  const factory TasksEvent.taskUpdated(Task task) = TaskUpdated;

  const factory TasksEvent.taskCreated(Task task) = TaskCreated;

  const factory TasksEvent.taskResolved(Task task) = TaskResolved;
}
      
      



, TasksBloc



  . , TasksBloc



, , map



:





@override
Stream<TasksState> mapEventToState(TasksEvent event) => event.map(
      fetchRequested: _mapFetchRequested,
      fetchCompleted: _mapFetchCompleted,
      filtersUpdated: _mapFiltersUpdated,
      taskUpdated: _mapTaskUpdated,
      taskCreated: _mapTaskCreated,
      taskResolved: _mapTaskResolved,
    );

Stream<TasksState> _mapTaskCreated(TaskCreated event) async* {
  // ...
}
      
      



( ) , , .





, , , . .





built_collection





, , BuiltMap



  BuiltList



 + , Builder.





- :





yield state.copyWith(
  tasks: state.tasks.rebuild((b) => b[createdTask.id] = createdTask),
);
      
      



flutter_bloc





, BLoC. - :





@freezed
abstract class TasksState implements _$TasksState {
  const factory TasksState({
    @required ProcessingState<TaskFetchingError, EmptyResult> fetchingState,
    @required ProcessingState<Exception, EmptyResult> updateState,
    @required BuiltList<Department> departments,
    @required TaskFilters filters,
    @required BuiltMap<TaskId, Task> tasks,
  }) = _TasksState;

  const TasksState._();
}

@freezed
abstract class TasksEvent with _$TasksEvent {
  const factory TasksEvent.fetchRequested() = FetchRequested;

  const factory TasksEvent.fetchCompleted(Either<Exception, TasksData> result) =
      FetchCompleted;

  const factory TasksEvent.filtersUpdated(TaskFilters filters) = FiltersUpdated;

  const factory TasksEvent.taskUpdated(Task task) = TaskUpdated;

  const factory TasksEvent.taskCreated(Task task) = TaskCreated;

  const factory TasksEvent.taskResolved(Task task) = TaskResolved;
}

class TasksBloc extends Bloc<TasksEvent, TasksState> {
  @override
  TasksState get initialState => TasksState(
        tasks: BuiltMap<TaskId, Task>(),
        departments: BuiltList<Department>(),
        filters: TaskFilters());

  @override
  Stream<TasksState> mapEventToState(TasksEvent event) => event.map(
        fetchRequested: _mapFetchRequested,
        fetchCompleted: _mapFetchCompleted,
        filtersUpdated: _mapFiltersUpdated,
        taskUpdated: _mapTaskUpdated,
        taskCreated: _mapTaskCreated,
        taskResolved: _mapTaskResolved,
      );
  
  Stream<TasksState> _mapTaskCreated(TaskCreated event) async* {
    yield state.copyWith(updateState: const ProcessingState.loading());
    final result = await _createTask(event.task);
    yield* result.fold(
      _triggerUpdateError,
      (taskId) async* {
        final createdTask = event.task.copyWith(id: taskId);
        yield state.copyWith(
          tasks: state.tasks.rebuild((b) => b[createdTask.id] = createdTask),
        );
        yield* _triggerUpdateSuccess();
      },
    );
  }

  // ...
}
      
      



_mapTaskCreated



: "", _createTask



. , .





Either<Exception, TaskId>



, "", "", .





json_serializable





API. , , / DTO / Dart-.





, DTO :





@JsonSerializable()
class GetAllTasksRequest {
  GetAllTasksRequest({
    this.assigneeProfileIds,
    this.departmentIds,
    this.createdUtc,
    this.deadlineUtc,
    this.closedUtc,
    this.state,
    this.extent,
  });

  final List<String> assigneeProfileIds;
  final List<String> departmentIds;
  final TimePeriodDto createdUtc;
  final TimePeriodDto deadlineUtc;
  final TimePeriodDto closedUtc;
  final TaskStateFilter state;
  final ExtentDto extent;

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



retrofit





API.





Android, . – , , :





@RestApi()
abstract class RestClient {
  factory RestClient(Dio dio) = _RestClient;

  @anonymous
  @POST('/api/general/v1/users/signIn')
  Future<SignInResponse> signIn(@Body() SignInRequest request);

  @anonymous
  @POST('/api/general/v1/users/resetPassword')
  Future<EmptyResponse> resetPassword(
    @Body() ResetPasswordRequestDto request,
  );

  @POST('/api/commander/v1/tasks/getAll')
  Future<GetAllTasksResponseDto> getTasks(@Body() GetAllTasksRequest request);

  @POST('/api/commander/v1/tasks/add')
  Future<TaskDto> createTask(@Body() CreateTaskDto request);
}

const anonymous = Extra({'isAnonymous': true});
      
      



provider





, , .





– , , : , , ..









Dart', dartfmt



, . , , ", dartfmt



". , , ( ). , CI-, PR' . , , 80 . :





β€œβ€¦for chrissake, don’t try to make 80 columns some immovable standard.”

Linus Torvalds





, dartfmt



  -l



 ( , lines_longer_than_80_chars



). , 120 – .









Dart' – . , . – .





, / (//).





, , , ( , , , ); , CI- PR.





, :





  • pedantic – ;





  • effective_dart – Effective Dart;





  • mews_pedantic – .





CI/CD

CI/CD, : " , ". Azure Pipelines ( ), , , Flutter, . , , Flutter', . – YAML bash-.





, Flutter', - :





  • Bitrise – 1 , 30 200 .





  • Codemagic – 500 , macOS 120 , .





  • Appcircle. 1 25- -.





, Appcircle, Bitrise Codemagic AWS device farm – .. UI- ( ).





- Codemagic – , .





GitHub Actions, , Azure Pipelines – Flutter. 500 MB 2.000 , : macOS ( , , iOS), 10! .., macOS-, 2.000 , 200.





Flutter.





– . Dart' , . , , . , sentry.





, , Flutter – - , . , Flutter . , , ( ). , – - Flutter .





text ellipsizing ( - ?) , , .





( , , ) – NoSuchMethodError



 (, Java NullPointerException



). , , Flutter' , – , .





( ). , ( , iOS ). , : " ? IDE ? flutter clean



 ? ?" – . , , , ( , Xcode).





, ?

. " "? , ? ?





, . . , Google . Flutter . UI – UI- Android-, . ...





: 4 ( ). , , . Android-, Flutter . , , ( ).





To be honest, I'm not a Dart fan. I really miss the capabilities of Kotlin, but code generation and the mentioned libraries partially save. If you try, even business logic can be written at a pretty decent level. And the ability to write once and run everywhere (including the UI) outweighs many disadvantages. Without Flutter, we would need at least 1.5 times more developers - with all that it implies.





Flutter is certainly not a silver bullet. She's not there at all, they say. Flutter is a tool, and when used as intended, it is a great tool.








All Articles