1 año con Flutter en producción

Esta es una versión de texto de mi presentación en DartUp 2020 (en inglés). En él, comparto los problemas que encontramos, analizo nuestro enfoque arquitectónico, hablo sobre bibliotecas útiles y respondo a la pregunta de si esta idea tuvo éxito: tomar y reescribir todo.





¿Que estamos haciendo?

Nuestro principal producto es un sistema de gestión hotelera. Grande y complejo. También hay algunos productos más pequeños, uno de los cuales es una aplicación móvil diseñada principalmente para el personal del hotel. Inicialmente, era una aplicación nativa para Android e iOS, pero hace aproximadamente un año y medio decidimos reescribirla en Flutter. Y lo reescribieron.





Primero, unas palabras sobre la aplicación en sí.





En general, esta es la aplicación B2B más común con todo lo que puede esperar de ella: autorización, gestión de perfiles, mensajes y tareas, formularios e interacción con el 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 . , , ( ).





Para ser honesto, no soy fan de Dart. Me mucho de menos las capacidades de Kotlin, pero la generación de código y las bibliotecas mencionadas parcialmente en Guardar. Si lo intenta, incluso la lógica empresarial se puede escribir a un nivel bastante decente. Y la capacidad de escribir una vez y ejecutarse en todas partes (incluida la interfaz de usuario) supera muchas desventajas. Sin Flutter, necesitaríamos al menos 1,5 veces más desarrolladores, con todo lo que implica.





Flutter ciertamente no es una solución milagrosa. Ella no está allí en absoluto, dicen. Flutter es una herramienta, y cuando se usa según lo previsto, es una gran herramienta.








All Articles