1 an avec Flutter en production

Ceci est une version texte de ma présentation à DartUp 2020 (en anglais). Dans ce document, je partage les problèmes que nous avons rencontrés, discute de notre approche architecturale, parle de bibliothèques utiles et répond à la question de savoir si cette idée a réussi - tout prendre et tout réécrire.





Que faisons-nous?

Notre produit principal est un système de gestion hôtelière. Grand et complexe. Il existe également quelques produits plus petits, dont une application mobile conçue principalement pour le personnel de l'hôtel. Au départ, c'était une application native pour Android et iOS, mais il y a environ un an et demi, nous avons décidé de la réécrire dans Flutter. Et ils l'ont réécrit.





Tout d'abord, quelques mots sur l'application elle-même.





En général, il s'agit de l'application B2B la plus courante avec tout ce que vous pouvez en attendre: autorisation, gestion des profils, messages et tâches, formulaires et interaction avec le 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 . , , ( ).





Pour être honnête, je ne suis pas fan de Dart. Les capacités de Kotlin me manquent vraiment , mais la génération de code et les bibliothèques mentionnées enregistrent partiellement. Si vous essayez, même la logique métier peut être écrite à un niveau assez décent. Et la possibilité d'écrire une fois et de s'exécuter partout (y compris l'interface utilisateur) l'emporte sur de nombreux inconvénients. Sans Flutter, nous aurions besoin d'au moins 1,5 fois plus de développeurs - avec tout ce que cela implique.





Flutter n'est certainement pas une solution miracle. Elle n'est pas du tout là, disent-ils. Flutter est un outil, et lorsqu'il est utilisé comme prévu, c'est un excellent outil.








All Articles