1 Jahr mit Flutter in Produktion

Dies ist eine Textversion meiner Präsentation auf der DartUp 2020 (in englischer Sprache). Darin teile ich die Probleme, auf die wir gestoßen sind, diskutiere unseren architektonischen Ansatz, spreche über nützliche Bibliotheken und beantworte die Frage, ob diese Idee erfolgreich war - alles zu übernehmen und neu zu schreiben.





Was machen wir?

Unser Hauptprodukt ist ein Hotelmanagementsystem. Groß und komplex. Es gibt auch einige kleinere Produkte, von denen eines eine mobile Anwendung ist, die hauptsächlich für Hotelmitarbeiter entwickelt wurde. Ursprünglich war es eine native App für Android und iOS, aber vor ungefähr anderthalb Jahren haben wir beschlossen, sie in Flutter neu zu schreiben. Und sie haben es umgeschrieben.





Zunächst ein paar Worte zur Anwendung.





Im Allgemeinen ist dies die häufigste B2B-Anwendung mit allem, was Sie von ihr erwarten kÜnnen: Autorisierung, Profilverwaltung, Nachrichten und Aufgaben, Formulare und Interaktion mit dem 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 . , , ( ).





Um ehrlich zu sein, bin ich kein Dart-Fan. Ich vermisse die Fähigkeiten von Kotlin sehr, aber die Codegenerierung und die genannten Bibliotheken sparen teilweise. Wenn Sie es versuchen, kann sogar Geschäftslogik auf einem ziemlich anständigen Niveau geschrieben werden. Und die Fähigkeit, einmal zu schreiben und überall auszuführen (einschließlich der Benutzeroberfläche), überwiegt viele Nachteile. Ohne Flutter würden wir mindestens 1,5-mal mehr Entwickler brauchen - mit allem, was dazu gehört.





Flattern ist sicherlich keine Silberkugel. Sie ist überhaupt nicht da, sagen sie. Flattern ist ein Werkzeug, und wenn es bestimmungsgemäß verwendet wird, ist es ein großartiges Werkzeug.








All Articles