Ini adalah versi teks dari presentasi saya di DartUp 2020 (dalam bahasa Inggris). Di dalamnya, saya berbagi masalah yang kami temui, mendiskusikan pendekatan arsitektural kami, berbicara tentang perpustakaan yang berguna, dan menjawab pertanyaan apakah ide ini berhasil - mengambil dan menulis ulang semuanya.
Apa yang kita lakukan?
Produk utama kami adalah sistem manajemen hotel. Besar dan kompleks. Ada juga beberapa produk yang lebih kecil, salah satunya adalah aplikasi seluler yang dirancang khusus untuk staf hotel. Awalnya, ini adalah aplikasi asli untuk Android dan iOS, tetapi sekitar satu setengah tahun yang lalu, kami memutuskan untuk menulis ulang di Flutter. Dan mereka menulis ulang.
Pertama, beberapa kata tentang aplikasi itu sendiri.
Secara umum, ini adalah aplikasi B2B paling umum dengan segala sesuatu yang dapat Anda harapkan darinya: otorisasi, manajemen profil, pesan dan tugas, formulir dan interaksi dengan 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 . , , ( ).
Sejujurnya, saya bukan penggemar Dart. Saya benar-benar merindukan kemampuan Kotlin, tetapi pembuatan kode dan pustaka yang disebutkan disimpan sebagian. Jika Anda mencobanya, bahkan logika bisnis dapat ditulis pada tingkat yang cukup baik. Dan kemampuan untuk menulis sekali dan dijalankan di mana-mana (termasuk UI) melebihi banyak kerugiannya. Tanpa Flutter, kami membutuhkan setidaknya 1,5 kali lebih banyak pengembang - dengan semua yang tersirat.
Flutter jelas bukan peluru perak. Dia sama sekali tidak ada di sana, kata mereka. Flutter adalah sebuah alat, dan jika digunakan sebagaimana mestinya, itu adalah alat yang hebat.