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 â , , - 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 . , , ( ).
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.