This repo base on management package is flutter_bloc. The app has been setup to work with retrofit, dio, json_annotation, intl_utils and shimmer
- Install Flutter SDK. Require Flutter >=3.3.0
- Install plugins in Android Studio (optional)
- Clone the repo.
- Run
flutter pub get
- Run
flutter pub run intl_utils:generate
- Run
flutter pub run build_runner build --delete-conflicting-outputs
- Run app.
assets
└───font
└───image
└───2.0x
└───3.0x
libs
└───bloc
│ └───app_cubit.dart
│ └───app_state.dart
└───common
│ └───app_colors.dart
│ └───app_dimens.dart
│ └───app_images.dart
│ └───app_shadows.dart
│ └───app_text_styles.dart
│ └───app_themes.dart
└───configs
│ └───app_configs.dart
└───database
│ └───secure_storage_helper.dart
│ └───shared_preferences_helper.dart
│ └───...
└───l10n
└───models
│ └───entities
│ │ └───user_entity.dart
│ │ └───...
│ └───enums
│ │ └───load_status.dart
│ │ └───...
│ └───params
│ │ └───sign_up_param.dart
│ │ └───...
│ └───response
│ └───array_response.dart
│ └───object_response.dart
└───networks
│ └───api_client.dart
│ └───api_interceptors.dart
│ └───api_util.dart
└───router
│ └───route_config.dart
└───repositories
│ └───auth_repository.dart
│ └───user_repository.dart.dart
│ └───...
└───ui
│ └───commons
│ │ └───app_bottom_sheet.dart
│ │ └───app_dialog.dart
│ │ └───app_snackbar.dart
│ │ └───...
│ └───pages
│ │ └───splash
│ │ │ └───splash_page.dart
│ │ │ └───splash_cubit.dart
│ │ │ └───splash_state.dart
│ │ └───...
│ └───widget //Chứa các widget base cho app
│ └───appbar
│ └───buttons
│ │ └───app_button.dart
│ │ └───app_icon_button.dart
│ │ └───...
│ └───images
│ │ └───app_cache_image.dart
│ │ └───app_circle_avatar.dart
│ └───textfields
│ └───shimmer
│ └───...
└───utils
│ └───date_utils.dart
│ └───file_utils.dart
│ └───logger.dart
│ └───utils.dart
│───main.dart
│───main_dev.dart //Config môi trường dev
└───main_staging.dart //Config môi trường production
Item | Explaint |
---|---|
main.dart: | the "entry point" of program. |
assets: | store static assests like fonts and images. |
common: | contain colors, textStyle, theme, ... |
configs: | hold the configs of your application. |
database: | container database helper class |
l10n: | contain all localized string. See more |
models: | contain entity, enum, .. |
networks: | |
router: | contain the route navigation |
repositories: | contain repository |
ui | |
utils |
All screen should be created in the ui/pages
folder
Each screen have 3 file:
Logic: movies_cubit.dart
class MoviesCubit extends Cubit<MoviesState> {
MovieRepository movieRepo;
MoviesCubit({
required this.movieRepo,
}) : super(const MoviesState());
void fetchMovies() async {
emit(state.copyWith(loadMovieStatus: LoadStatus.loading));
try {
final result = await movieRepo.getMovies();
emit(state.copyWith(
loadMovieStatus: LoadStatus.success,
movies: result.results,
));
} catch (e) {
emit(state.copyWith(loadMovieStatus: LoadStatus.failure));
}
}
}
State: movies_state.dart
class MoviesState extends Equatable {
final LoadStatus loadMovieStatus;
final List<MovieEntity> movies;
const MoviesState({
this.loadMovieStatus = LoadStatus.initial,
this.movies = const [],
});
@override
List<Object?> get props => [
loadMovieStatus,
movies,
];
MoviesState copyWith({
LoadStatus? loadMovieStatus,
List<MovieEntity>? movies,
}) {
return MoviesState(
loadMovieStatus: loadMovieStatus ?? this.loadMovieStatus,
movies: movies ?? this.movies,
);
}
}
View: movies_view.dart
class MoviesPage extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return MoviesCubit(
movieRepo: context.read<MovieRepository>(),
);
},
child: MoviesChildPage(),
);
}
}
class MoviesChildPage extends StatefulWidget {
...
@override
State<MoviesChildPage> createState() => _MoviesChildPageState();
}
class _MoviesChildPageState extends State<MoviesChildPage> {
late MoviesCubit _cubit;
@override
void initState() {
_cubit = BlocProvider.of<MoviesCubit>(context);
super.initState();
_cubit.fetchInitialMovies();
}
@override
Widget build(BuildContext context) {
super.build(context);
return ...;
}
...
}
- Create entity object in folder
lib/models/entities
Ex:movie_entity.dart
import 'package:json_annotation/json_annotation.dart';
part 'movie_entity.g.dart';
@JsonSerializable()
class MovieEntity {
@JsonKey()
String? title;
...
factory MovieEntity.fromJson(Map<String, dynamic> json) => _$MovieEntityFromJson(json);
Map<String, dynamic> toJson() => _$MovieEntityToJson(this);
}
Class must have @JsonSerializable()
for generator. Read json_serializable
- Define and Generate your API in file
lib/networks/api_client.dart
Ex: GET movies
/// Movie
@GET("/3/discover/movie")
Future<ArrayResponse<MovieEntity>> getMovies(@Query('api_key') String apiKey, @Query('page') int page);
Note: Using ArrayResponse and ObjectResponse for generic response
- Require run command line:
flutter pub run build_runner build --delete-conflicting-outputs
- Create repository file for your feature in folder
lib/repositories
Ex:movie_repository.dart
abstract class MovieRepository {
Future<ArrayResponse<MovieEntity>> getMovies();
}
class MovieRepositoryImpl extends MovieRepository {
ApiClient apiClient;
MovieRepositoryImpl({required this.apiClient});
@override
Future<ArrayResponse<MovieEntity>> getMovies() async {
return apiClient.getMovies(...);
}
}
After, add part 'auth_api.dart';
to services/api/api_service
- You can call API in the logic of screen. Ex:
final result = await movieRepo.getMovies();
logger.d("message"); //"💙 DEBUG: message"
logger.i("message"); //"💚 INFO: message"
logger.e("message"); //"❤️ ERROR: message"
logger.log("very very very long message");
AppSnackbar.showInfo(message: 'Info');
AppSnackbar.showWarning(message: 'Warning');
AppSnackbar.showError(message: 'Error');
AppDialog.defaultDialog(
message: "An error happened. Please check your connection!",
textConfirm: "Retry",
onConfirm: () {
//Do something
},
);
return Obx(() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: AppTintButton(
title: 'Sign In',
onPressed: _signIn,
isLoading: state.signInStatus.value == LoadStatus.loading,
),
);
});