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 – , , - 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* {
// ...
}
( ) , , .
, , , . .
, , BuiltMap
BuiltList
+ , Builder.
- :
yield state.copyWith(
tasks: state.tasks.rebuild((b) => b[createdTask.id] = createdTask),
);
, 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>
, "", "", .
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);
}
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});
, , .
– , , : , , ..
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', - :
, 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.