This commit is contained in:
petrovyuri
2025-01-21 14:24:31 +03:00
parent e7b2c31e86
commit 17d096baac
96 changed files with 3575 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "17025dd88227cd9532c33fa78f5250d548d87e9a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
- platform: android
create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

23
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "dev",
"type": "dart",
"request": "launch",
"program": "${workspaceFolder}/lib/targets/dev.dart",
},
{
"name": "stage",
"type": "dart",
"request": "launch",
"program": "${workspaceFolder}/lib/targets/stage.dart",
},
{
"name": "prod",
"type": "dart",
"request": "launch",
"program": "${workspaceFolder}/lib/targets/prod.dart",
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build_runner --delete-conflicting-outputs",
"type": "shell",
"command": "dart",
"args": [
"run",
"build_runner",
"build",
"--delete-conflicting-outputs"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always"
},
"problemMatcher": []
},
{
"label": "build_runner clean",
"type": "shell",
"command": "dart",
"args": [
"run",
"build_runner",
"clean"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always"
},
"problemMatcher": []
}
]
}

16
README copy.md Normal file
View File

@@ -0,0 +1,16 @@
# friflex_starter
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

10
analysis_options.yaml Normal file
View File

@@ -0,0 +1,10 @@
include: package:friflex_lint_rules/analysis_options.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.yaml"
- "app_services/aurora/**"
- "/app_services/aurora/**"
- "**/app_services/aurora/**"

13
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle Normal file
View File

@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.pym.miniapp.friflex_starter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.pym.miniapp.friflex_starter"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="friflex_starter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.pym.miniapp.friflex_starter
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

25
android/settings.gradle Normal file
View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "7482962148e8d758338d8a28f589f317e1e42ba4"
channel: "stable"
project_type: package

View File

@@ -0,0 +1 @@
# Базовые сервисы для приложения

View File

@@ -0,0 +1 @@
include: package:friflex_lint_rules/analysis_options.yaml

View File

@@ -0,0 +1,4 @@
library app_services;
export 'src/app_path_provider.dart';
export 'src/app_secure_storage.dart';

View File

@@ -0,0 +1,13 @@
import 'package:i_app_services/i_app_services.dart';
import 'package:path_provider/path_provider.dart';
/// Класс для Aurora реализации сервиса работы с путями
class AppPathProvider implements IPathProvider {
/// Наименование сервиса
static const name = 'AuroraAppPathProvider';
@override
Future<String> getAppDocumentsDirectoryPath() async {
return (await getApplicationDocumentsDirectory()).path;
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_secure_storage_aurora/flutter_secure_storage_aurora.dart';
import 'package:i_app_services/i_app_services.dart';
/// Класс для Aurora реализации сервис по работе с защищенным хранилищем
final class AppSecureStorage implements ISecureStorage {
/// Создает сервис для работы с защищенным хранилищем
///
/// Принимает:
/// - [secretKey] - ключ шифрования данных
AppSecureStorage({required this.secretKey}){
FlutterSecureStorageAurora.setSecret(secretKey);
}
@override
final String secretKey;
@override
String get name => 'AuroraAppSecureStorage';
/// Экземпляр хранилища данных
final _box = const FlutterSecureStorage();
@override
Future<void> clear() async {
await _box.deleteAll();
}
@override
Future<void> delete(String key) async {
await _box.delete(key: key);
}
@override
Future<bool> exists(String key) {
return _box.containsKey(key: key);
}
@override
Future<String?> read(String key) async {
return _box.read(key: key);
}
@override
Future<void> write(String key, String value) async {
await _box.write(key: key, value: value);
}
}

View File

@@ -0,0 +1,37 @@
name: app_services
description: "Google сервисы для приложения"
version: 0.0.1
publish_to: none
environment:
sdk: ^3.5.0
flutter: ^3.24.0
dependencies:
flutter:
sdk: flutter
# Зависимости для сервиса защищенного хранилища
flutter_secure_storage: 8.0.0
flutter_secure_storage_aurora:
git:
url: https://gitlab.com/omprussia/flutter/flutter-community-plugins/flutter_secure_storage_aurora.git
ref: aurora-0.5.3
# для работы с путями в хранилища
path_provider: 2.1.4
path_provider_aurora:
git:
url: https://gitlab.com/omprussia/flutter/packages.git
ref: aurora-path_provider_aurora-0.6.0
path: packages/path_provider_aurora
# Обязательные интерфейсы
i_app_services:
path: ../../i_app_services
dev_dependencies:
friflex_lint_rules:
hosted: https://pub.friflex.com
version: 4.0.1

View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "7482962148e8d758338d8a28f589f317e1e42ba4"
channel: "stable"
project_type: package

View File

@@ -0,0 +1 @@
# Базовые сервисы для приложения

View File

@@ -0,0 +1 @@
include: package:friflex_lint_rules/analysis_options.yaml

View File

@@ -0,0 +1,4 @@
library app_services;
export 'src/app_path_provider.dart';
export 'src/app_secure_storage.dart';

View File

@@ -0,0 +1,13 @@
import 'package:i_app_services/i_app_services.dart';
import 'package:path_provider/path_provider.dart';
/// Класс для базовой реализации сервиса работы с путями
class AppPathProvider implements IPathProvider {
/// Наименование сервиса
static const name = 'GmsAppPathProvider';
@override
Future<String> getAppDocumentsDirectoryPath() async {
return (await getApplicationDocumentsDirectory()).path;
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:i_app_services/i_app_services.dart';
/// Класс для базовой реализации сервис по работе с защищенным хранилищем
final class AppSecureStorage implements ISecureStorage {
/// Создает сервис для работы с защищенным хранилищем
///
/// Принимает:
/// - [secretKey] - ключ шифрования данных
AppSecureStorage({required this.secretKey});
@override
final String secretKey;
static const name = 'GmsAppSecureStorage';
/// Экземпляр хранилища данных
final _box = const FlutterSecureStorage();
@override
Future<void> clear() async {
await _box.deleteAll();
}
@override
Future<void> delete(String key) async {
await _box.delete(key: key);
}
@override
Future<bool> exists(String key) {
return _box.containsKey(key: key);
}
@override
Future<String?> read(String key) async {
return _box.read(key: key);
}
@override
Future<void> write(String key, String value) async {
await _box.write(key: key, value: value);
}
}

View File

@@ -0,0 +1,35 @@
name: app_services
description: "Google сервисы для приложения"
version: 0.0.1
publish_to: none
environment:
sdk: ^3.5.0
flutter: ^3.24.0
dependencies:
flutter:
sdk: flutter
# Зависимости для сервиса логирования
talker_flutter: 4.6.4
talker_dio_logger: 4.6.4
talker_bloc_logger: 4.6.4
# Зависимости для сервиса защищенного хранилища
flutter_secure_storage: 9.2.4
# Зависимости для сервиса незащищенного хранилища
shared_preferences: 2.3.5
# для работы с путями в хранилища
path_provider: 2.1.5
# Обязательные интерфейсы
i_app_services:
path: ../../i_app_services
dev_dependencies:
friflex_lint_rules:
hosted: https://pub.friflex.com
version: 4.0.1

29
app_services/i_app_services/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View File

@@ -0,0 +1 @@
# Хранит в себе все интерфейсы для реализации общих сервисов

View File

@@ -0,0 +1,10 @@
include: package:friflex_lint_rules/analysis_options.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.yaml"
- "app_services/aurora/**"
- "/app_services/aurora/**"
- "**/app_services/aurora/**"

View File

@@ -0,0 +1,4 @@
library i_app_services;
export 'src/i_path_provider.dart';
export 'src/i_secure_storage.dart';

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
/// Интерфейс для сервиса отладки
abstract interface class IDebugService {
/// Наименование сервиса
static const name = 'IDebugService';
/// Метод для создания обработчика для BLoC
Object createBlocObserver();
/// Метод для создания обработчика для роутера
NavigatorObserver createRouterObserver();
/// Метод для создания обработчика для http-клиентов
Object createHttpInterceptor();
/// Метод для логгирования предупреждений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void warning(String message);
/// Метод для логгирования ошибок
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
/// - [exception] - исключение
/// - [stackTrace] - стек вызова
void error(String message, [Object? exception, StackTrace? stackTrace]);
/// Метод для обработки ошибок
///
/// Принимает:
/// - [error] - исключение
/// - [stackTrace] - стек вызова
/// - [message] - сообщение для логгирования в формате [String]
void handleError(Object error, [StackTrace? stackTrace, String? message]);
/// Метод для логгирования информативных сообщений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void info(String message);
/// Метод для логгирования сообщений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void log(String message);
/// Метод для открытия окна отладки
///
/// Принимает:
/// - [context] - для определения навигатора по нему
/// - [useRootNavigator] - при true, открывает окно в корневом навигаторе
Future<T?> openDebugScreen<T>(
BuildContext context, {
bool useRootNavigator = false,
});
}

View File

@@ -0,0 +1,9 @@
/// Класс для описания интерфейса сервиса
/// для получения пути хранения файлов
abstract interface class IPathProvider {
/// Наименования интерфейса
static const name = 'IPathProvider';
/// Получение path на внутренне хранилище приложения
Future<String> getAppDocumentsDirectoryPath();
}

View File

@@ -0,0 +1,44 @@
/// Класс интерфейса для работы с защищенным хранилищем
abstract interface class ISecureStorage {
/// Описывает обязательные параметры имплементаций
///
/// Требует:
/// - [secretKey] - секретный ключ для шифрования данных
const ISecureStorage._({
required this.secretKey,
});
/// Секретный ключ для шифрования данных
final String secretKey;
/// Наименования интерфейса
static const name = 'ISecureStorage';
/// Метод для получения значения из защищенного хранилища
///
/// Принимает:
/// - [key] - ключ
Future<String?> read(String key);
/// Метод для записи значения в защищенное хранилище
///
/// Принимает:
/// - [key] - ключ
/// - [value] - значение
Future<void> write(String key, String value);
/// Метод для удаления значения из защищенного хранилища
///
/// Принимает:
/// - [key] - ключ
Future<void> delete(String key);
/// Метод для очистки защищенного хранилища
Future<void> clear();
/// Метод для проверки наличия значения в защищенном хранилище
///
/// Принимает:
/// - [key] - ключ
Future<bool> exists(String key);
}

View File

@@ -0,0 +1,17 @@
name: i_app_services
description: "Хранит в себе все интерфейсы для реализации общих сервисов"
version: 0.0.1
publish_to: "none"
environment:
sdk: ^3.5.0
flutter: ^3.24.0
dependencies:
flutter:
sdk: flutter
dev_dependencies:
friflex_lint_rules:
hosted: https://pub.friflex.com
version: 4.0.1

3
env/dev.env vendored Normal file
View File

@@ -0,0 +1,3 @@
baseUrl="https://dev"
secretKey="dev"

2
env/prod.env vendored Normal file
View File

@@ -0,0 +1,2 @@
baseUrl="https://prod"
secretKey="prod"

2
env/stage.env vendored Normal file
View File

@@ -0,0 +1,2 @@
baseUrl="https://stage"
secretKey="stage"

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

89
lib/app/app.dart Normal file
View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_theme.dart';
import 'package:friflex_starter/app/theme/theme_notifier.dart';
import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/l10n/localization_notifier.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
/// Класс для реализации объекта приложения
class App extends StatelessWidget {
/// Создает экземпляр приложения
///
/// Принимает:
/// - [diContainer] - набор зависимостей приложения
/// - [router] - экземпляр роутера приложения
const App({
super.key,
required this.diContainer,
required this.router,
});
/// Набор зависимостей приложения
final DiContainer diContainer;
/// Экземпляр роутера приложения
final GoRouter router;
@override
Widget build(BuildContext context) {
return AppProviders(
diContainer: diContainer,
child: LocalizationConsumer(
builder: () => ThemeConsumer(
builder: () => _App(router: router),
),
),
);
}
}
/// Класс для реализации объекта приложения
class _App extends StatelessWidget {
const _App({required this.router});
final GoRouter router;
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
darkTheme: AppTheme.dark,
theme: AppTheme.light,
themeMode: context.theme.themeMode,
locale: context.localization.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
}
}
/// Класс для реализации провайдеров приложения
final class AppProviders extends StatelessWidget {
const AppProviders({
super.key,
required this.child,
required this.diContainer,
});
final Widget child;
final DiContainer diContainer;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider.value(value: diContainer), // Передаем контейнер зависимостей
ChangeNotifierProvider(
create: (_) => ThemeNotifier(),
), // Провайдер для темы
ChangeNotifierProvider(
create: (_) => LocalizationNotifier(),
), // Провайдер для локализации
],
child: child,
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:envied/envied.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart';
part 'app_config.g.dart';
/// Класс для реализации конфигурации с моковыми данными
@Envied(name: 'Dev', path: 'env/dev.env')
class AppConfigDev implements IAppConfig {
@override
AppEnv get env => AppEnv.dev;
@override
String get name => 'AppConfigDev';
@override
@EnviedField()
final String baseUrl = _Dev.baseUrl;
@override
@EnviedField(obfuscate: true)
final String secretKey = _Dev.secretKey;
}
/// Класс для реализации конфигурации с продакшн данными
@Envied(name: 'Prod', path: 'env/prod.env')
class AppConfigProd implements IAppConfig {
@override
AppEnv get env => AppEnv.prod;
@override
String get name => 'AppConfigProd';
@override
@EnviedField(obfuscate: true)
final String baseUrl = _Prod.baseUrl;
@override
@EnviedField(obfuscate: true)
final String secretKey = _Prod.secretKey;
}
/// Класс для реализации конфигурации с стейдж данными
@Envied(name: 'Stage', path: 'env/stage.env')
class AppConfigStage implements IAppConfig {
@override
AppEnv get env => AppEnv.stage;
@override
String get name => 'AppConfigStage';
@override
@EnviedField(obfuscate: true)
final String baseUrl = _Stage.baseUrl;
@override
@EnviedField(obfuscate: true)
final String secretKey = _Stage.secretKey;
}

View File

@@ -0,0 +1,158 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_config.dart';
// **************************************************************************
// EnviedGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// generated_from: env/dev.env
final class _Dev {
static const String baseUrl = 'https://dev';
static const List<int> _enviedkeysecretKey = <int>[
45820206,
4292305074,
1553598735,
];
static const List<int> _envieddatasecretKey = <int>[
45820234,
4292305111,
1553598841,
];
static final String secretKey = String.fromCharCodes(List<int>.generate(
_envieddatasecretKey.length,
(int i) => i,
growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
}
// coverage:ignore-file
// ignore_for_file: type=lint
// generated_from: env/prod.env
final class _Prod {
static const List<int> _enviedkeybaseUrl = <int>[
3619294633,
560029786,
3178585068,
3377720392,
977735066,
2142081055,
1298585806,
933917938,
1244996901,
1950368931,
2147265964,
2338251746,
];
static const List<int> _envieddatabaseUrl = <int>[
3619294657,
560029742,
3178584984,
3377720376,
977735145,
2142081061,
1298585825,
933917917,
1244996949,
1950368977,
2147265987,
2338251654,
];
static final String baseUrl = String.fromCharCodes(List<int>.generate(
_envieddatabaseUrl.length,
(int i) => i,
growable: false,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]));
static const List<int> _enviedkeysecretKey = <int>[
2449171331,
2315988352,
1037757119,
3159274193,
];
static const List<int> _envieddatasecretKey = <int>[
2449171443,
2315988466,
1037757136,
3159274165,
];
static final String secretKey = String.fromCharCodes(List<int>.generate(
_envieddatasecretKey.length,
(int i) => i,
growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
}
// coverage:ignore-file
// ignore_for_file: type=lint
// generated_from: env/stage.env
final class _Stage {
static const List<int> _enviedkeybaseUrl = <int>[
443716089,
3928907238,
1851881210,
3858110087,
3324475128,
1601592105,
2404110281,
1092690431,
1025677374,
3283672546,
425122182,
3412521909,
1297182020,
];
static const List<int> _envieddatabaseUrl = <int>[
443715985,
3928907154,
1851881102,
3858110199,
3324475019,
1601592083,
2404110310,
1092690384,
1025677389,
3283672470,
425122279,
3412521938,
1297181985,
];
static final String baseUrl = String.fromCharCodes(List<int>.generate(
_envieddatabaseUrl.length,
(int i) => i,
growable: false,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]));
static const List<int> _enviedkeysecretKey = <int>[
58874248,
3497500657,
3833421599,
555777488,
132619188,
];
static const List<int> _envieddatasecretKey = <int>[
58874363,
3497500549,
3833421694,
555777463,
132619217,
];
static final String secretKey = String.fromCharCodes(List<int>.generate(
_envieddatasecretKey.length,
(int i) => i,
growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
}

View File

@@ -0,0 +1,16 @@
import 'package:friflex_starter/app/app_env.dart';
/// Класс для описания интерфейса конфигурации
abstract interface class IAppConfig {
/// Наименование сервиса
String get name => 'IAppConfig';
/// Основной адрес для запросов к API
String get baseUrl;
/// Тип окружения
AppEnv get env;
/// Секретный ключ для шифрования данных
String get secretKey;
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:friflex_starter/app/theme/theme_notifier.dart';
import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/l10n/localization_notifier.dart';
import 'package:provider/provider.dart';
/// Класс, реализующий расширение для контекста приложения
extension AppContextExt on BuildContext {
/// Метод для получения экземпляра DIContainer
DiContainer get di => read<DiContainer>();
/// Геттер для получения цветовой схемы
ColorScheme get colors => Theme.of(this).colorScheme;
/// Геттер для получения темы
ThemeNotifier get theme => read<ThemeNotifier>();
/// Геттер для получения локализации
AppLocalizations get l10n => AppLocalizations.of(this)!;
/// Геттер для получения управления локализацией
LocalizationNotifier get localization => read<LocalizationNotifier>();
}

12
lib/app/app_env.dart Normal file
View File

@@ -0,0 +1,12 @@
/// Перечислимый тип окружений сборки
enum AppEnv {
/// Тестовое окружение (моковое)
dev,
/// Стейдж окружение (тестовое окружение, которое имеет возможность
/// как обращаться в сеть, так и использовать моковые данные)
stage,
/// Продакшен окружение
prod,
}

View File

@@ -0,0 +1,146 @@
import 'package:dio/dio.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
/// Класс для реализации HTTP-клиента для управления запросами
final class AppHttpClient implements IHttpClient {
/// Создает HTTP клиент
///
/// Принимает:
/// - [debugService] - сервис для логирования запросов
/// - [appConfig] - конфигурация приложения
AppHttpClient({
required IDebugService debugService,
required IAppConfig appConfig,
}) {
_httpClient = Dio();
_appConfig = appConfig;
_httpClient.options
..baseUrl = appConfig.baseUrl
..connectTimeout = const Duration(seconds: 5)
..sendTimeout = const Duration(seconds: 7)
..receiveTimeout = const Duration(seconds: 10)
..headers = {
'Content-Type': 'application/json',
};
// Добавление интерцептора для логирования запросов
if (appConfig.env != AppEnv.prod) {
final interceptor = debugService.createHttpInterceptor();
if (interceptor is Interceptor) {
_httpClient.interceptors.add(interceptor);
}
}
}
/// Конфигурация приложения
late final IAppConfig _appConfig;
/// Экземпляр HTTP клиента
late final Dio _httpClient;
@override
Future<Response> get(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.get(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> post(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> patch(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.patch(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> put(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> delete(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> head(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.head(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
}

View File

@@ -0,0 +1,94 @@
import 'package:dio/dio.dart';
/// Класс для описания интерфейса сервиса по управлению HTTP запросами
abstract interface class IHttpClient {
/// Описывает поля HTTP клиента
const IHttpClient();
/// Наименование сервиса
static const name = 'IHttpClient';
/// Метод для реализации запроса GET
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> get(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса POST
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> post(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса PATCH
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> patch(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса PUT
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> put(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса DELETE
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> delete(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса POST
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> head(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
}

View File

@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
/// Класс, реализующий расширение для добавления токенов в цветовую схему
extension AppColorsScheme on ColorScheme {
bool get _isDark => brightness == Brightness.dark;
// Тестовый цвет
Color get testColor => _isDark ? Colors.green : Colors.red;
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
/// Класс для конфигурации светлой/темной темы приложения
abstract class AppTheme {
/// Геттер для получения светлой темы
static ThemeData get light => ThemeData.light();
/// Геттер для получения темной темы
static ThemeData get dark => ThemeData.dark();
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef ThemeBuilder = Widget Function();
/// Виджет для подписки на изменение темы приложения
class ThemeConsumer extends StatelessWidget {
const ThemeConsumer({super.key, required this.builder});
final ThemeBuilder builder;
@override
Widget build(BuildContext context) {
return Consumer<ThemeNotifier>(
builder: (_, __, ___) {
return builder();
},
);
}
}
/// Класс для управления темой приложения
final class ThemeNotifier extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
void changeTheme() {
_themeMode =
_themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
}

6
lib/di/di_base_repo.dart Normal file
View File

@@ -0,0 +1,6 @@
/// Миксин репозитория в приложении.
/// Каждый интерфейс репозитория в приложении должен подмешивать текущий класс
mixin class DiBaseRepo {
/// Наименование репозитория
String get name => 'DiBaseRepo';
}

103
lib/di/di_container.dart Normal file
View File

@@ -0,0 +1,103 @@
import 'package:app_services/app_services.dart';
import 'package:friflex_starter/app/app_config/app_config.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/di/di_repositories.dart';
import 'package:friflex_starter/di/di_typedefs.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
import 'package:i_app_services/i_app_services.dart';
/// {@template dependencies_container}
/// Контейнер для зависимостей
/// {@macro composition_process}
/// {@endtemplate}
final class DiContainer {
/// {@macro dependencies_container}
DiContainer({required this.env, required IDebugService dService})
: debugService = dService;
final AppEnv env;
/// Сервис для отладки, получаем из конструктора
late final IDebugService debugService;
/// Сервис для работы с путями
late final IPathProvider pathProvider;
/// Конфигурация приложения
late final IAppConfig appConfig;
/// Сервис для работы с локальным хранилищем
late final ISecureStorage secureStorage;
/// Сервис для работы с HTTP запросами
late final IHttpClient httpClient;
late final DiRepositories repositories;
/// Метод для инициализации зависимостей
Future<void> init({
required OnProgress onProgress,
required OnComplete onComplete,
required OnError onError,
}) async {
// Инициализация сервисов
await _initServices(
onComplete: onComplete,
onError: onError,
onProgress: onProgress,
);
// Инициализация репозиториев
repositories = DiRepositories();
repositories.init(
onProgress: onProgress,
onComplete: onComplete,
onError: onError,
diContainer: this,
);
onComplete('Инициализация зависимостей завершена!');
}
/// Метод для инициализации сервисов
Future<void> _initServices({
required OnComplete onComplete,
required OnError onError,
required OnProgress onProgress,
}) async {
appConfig = switch (env) {
AppEnv.dev => AppConfigDev(),
AppEnv.prod => AppConfigProd(),
AppEnv.stage => AppConfigStage()
};
httpClient = AppHttpClient(
debugService: debugService,
appConfig: appConfig,
);
try {
pathProvider = AppPathProvider();
onProgress(AppPathProvider.name);
} on Object catch (error, stackTrace) {
onError(
'Ошибка инициализации ${IPathProvider.name}',
error: error,
stackTrace: stackTrace,
);
}
try {
secureStorage = AppSecureStorage(secretKey: appConfig.secretKey);
onProgress(AppSecureStorage.name);
} on Object catch (error, stackTrace) {
onError(
'Ошибка инициализации ${ISecureStorage.name}',
error: error,
stackTrace: stackTrace,
);
}
}
}

115
lib/di/di_repositories.dart Normal file
View File

@@ -0,0 +1,115 @@
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/di/di_base_repo.dart';
import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/di/di_typedefs.dart';
import 'package:friflex_starter/features/auth/data/repository/auth_mock_repository.dart';
import 'package:friflex_starter/features/auth/data/repository/auth_repository.dart';
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
import 'package:friflex_starter/features/main/data/repository/main_mock_repository.dart';
import 'package:friflex_starter/features/main/data/repository/main_repository.dart';
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
/// Список названий моковых репозиториев, которые должны быть подменены
/// для использования в сборке stage окружения
///
/// Для того, чтобы репозиторий был автоматически подменен на моковый в stage
/// сборке, необходимо в этом списке указать название мокового репозитория,
/// обращаясь к соответствующему полю name.
///
/// Пример:
/// ```
/// [ AuthCheckRepositoryMock().name, ]
/// ```
final List<String> _mockReposToSwitch = [];
/// Класс для инициализации репозиториев в приложении
///
/// По умолчанию репозиторию присваивается моковая реализация.
/// В зависимости от окружения либо выполняется подмена репозиторий,
/// либо используется моковый.
final class DiRepositories {
/// Интерфейс для работы с репозиторием авторизации
late final IAuthRepository authRepository;
/// Интерфейс для работы с репозиторием главного сервиса
late final IMainRepository mainRepository;
/// Метод для инициализации репозиториев в приложении
///
/// Принимает:
/// - [onProgress] - обратный вызов при прогрессе
/// - [onComplete] - обратный вызов при успешной инициализации
/// - [diContainer] - контейнер зависимостей
void init({
required OnProgress onProgress,
required OnComplete onComplete,
required OnError onError,
required DiContainer diContainer,
}) {
try {
//Инициализация репозитория авторизации
authRepository = lazyInitRepo<IAuthRepository>(
mockFactory: AuthMockRepository.new,
mainFactory: () => AuthRepository(
httpClient: diContainer.httpClient,
),
onProgress: onProgress,
environment: diContainer.env,
);
onProgress(authRepository.name);
} on Object catch (error, stackTrace) {
onError(
'Ошибка инициализации репозитория $IAuthRepository',
error: error,
stackTrace: stackTrace,
);
}
try {
// Инициализация репозитория сервиса управления токеном доступа
mainRepository = lazyInitRepo<IMainRepository>(
mockFactory: MainMockRepository.new,
mainFactory: () => MainRepository(
httpClient: diContainer.httpClient,
),
onProgress: onProgress,
environment: diContainer.env,
);
onProgress(mainRepository.name);
} on Object catch (error, stackTrace) {
onError(
'Ошибка инициализации репозитория $IMainRepository',
error: error,
stackTrace: stackTrace,
);
}
onComplete(
'Инициализация репозиториев завершена! Было подменено репозиториев - ${_mockReposToSwitch.length} (${_mockReposToSwitch.join(', ')})',
);
}
/// Метод для ленивой инициализации конкретного репозитория по типу [Т].
/// В зависимости от окружения инициализируется моковый или сетевой репозиторий.
///
/// Принимает:
/// - [mockFactory] - функция - фабрика для инициализации репозитория для управления моковыми запросами
/// - [mainFactory] - функция - фабрика для инициализации основного репозиторий
/// - [onProgress] - обратный вызов при прогрессе
T lazyInitRepo<T extends DiBaseRepo>({
required AppEnv environment,
required T Function() mainFactory,
required T Function() mockFactory,
required OnProgress onProgress,
}) {
final repo = switch (environment) {
AppEnv.dev => mockFactory(),
AppEnv.prod => mainFactory(),
AppEnv.stage => _mockReposToSwitch.contains(mockFactory().name)
? mockFactory()
: mainFactory(),
};
onProgress(repo.name);
return repo;
}
}

12
lib/di/di_typedefs.dart Normal file
View File

@@ -0,0 +1,12 @@
/// Обратный вызов при ошибки инициализации
typedef OnError = void Function(
String message, {
Object? error,
StackTrace? stackTrace,
});
/// Обратный вызов при прогрессе
typedef OnProgress = void Function(String name);
/// Обратный вызов при успешной инициализации
typedef OnComplete = void Function(String msg);

View File

@@ -0,0 +1,9 @@
import '../../domain/repository/i_auth_repository.dart';
/// {@template AuthMockRepository}
///
/// {@endtemplate}
final class AuthMockRepository implements IAuthRepository {
@override
String get name => 'AuthMockRepository';
}

View File

@@ -0,0 +1,15 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_auth_repository.dart';
/// {@template AuthRepository}
///
/// {@endtemplate}
final class AuthRepository implements IAuthRepository {
final IHttpClient httpClient;
AuthRepository({required this.httpClient});
@override
String get name => 'AuthRepository';
}

View File

@@ -0,0 +1,6 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IAuthRepository}
///
/// {@endtemplate}
abstract interface class IAuthRepository with DiBaseRepo {}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
/// {@template AuthScreen}
///
/// {@endtemplate}
class AuthScreen extends StatelessWidget {
/// {@macro AuthScreen}
const AuthScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AuthScreen'),
),
body: const Center(
child: Text('AuthScreen'),
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
import 'package:talker_bloc_logger/talker_bloc_logger.dart';
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
import 'package:talker_flutter/talker_flutter.dart';
/// Класс реализации интерфейса дебаг сервиса
class AppDebugService implements IDebugService {
/// Наименование сервиса
static const name = 'GmsDebugService';
final Talker _talker = TalkerFlutter.init();
@override
TalkerBlocObserver createBlocObserver() =>
TalkerBlocObserver(talker: _talker);
@override
TalkerDioLogger createHttpInterceptor() => TalkerDioLogger(talker: _talker);
@override
TalkerRouteObserver createRouterObserver() => TalkerRouteObserver(_talker);
@override
void error(String msg, [Object? exception, StackTrace? stackTrace]) {
_talker.error(msg, exception, stackTrace);
}
@override
void handleError(Object error, [StackTrace? stackTrace, String? message]) {
_talker.handle(error, stackTrace, message);
}
@override
void info(String message) {
_talker.info(message);
}
@override
void log(String message) {
_talker.log(message);
}
@override
void warning(String message) {
_talker.warning(message);
}
@override
Future<T?> openDebugScreen<T>(
BuildContext context, {
bool useRootNavigator = false,
}) {
return Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) => TalkerScreen(talker: _talker),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:friflex_starter/features/debug/debug_screen.dart';
import 'package:go_router/go_router.dart';
abstract final class DebugRoutes {
/// Название роута страницы профиля пользователя
static const String debugScreenName = 'debug_screen';
/// Путь роута страницы профиля пользователя
static const String _debugScreenPath = '/debug';
/// Метод для построения ветки роутов по фиче профиля пользователя
///
/// Принимает:
/// - [routes] - вложенные роуты
static StatefulShellBranch buildShellBranch({
List<RouteBase> routes = const [],
List<NavigatorObserver>? observers,
}) =>
StatefulShellBranch(
initialLocation: _debugScreenPath,
observers: observers,
routes: [
GoRoute(
path: _debugScreenPath,
name: debugScreenName,
builder: (context, state) => const DebugScreen(),
routes: routes,
),
],
);
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
class DebugScreen extends StatelessWidget {
const DebugScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Debug Screen')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {
throw Exception(
'Тестовая ошибка Exception для отладки FlutterError',);
},
child: const Text('Вызывать ошибку FlutterError'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await callError();
},
child: const Text('Вызывать ошибку PlatformDispatcher'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await context.di.debugService.openDebugScreen(context);
},
child: const Text('Вызывать Talker'),
),
],
),
),
);
}
Future<void> callError() async {
throw Exception('Тестовая ошибка Exception для отладки PlatformDispatcher');
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
/// Интерфейс для сервиса отладки
abstract interface class IDebugService {
/// Наименование сервиса
static const name = 'IDebugService';
/// Метод для создания обработчика для BLoC
Object createBlocObserver();
/// Метод для создания обработчика для роутера
NavigatorObserver createRouterObserver();
/// Метод для создания обработчика для http-клиентов
Object createHttpInterceptor();
/// Метод для логгирования предупреждений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void warning(String message);
/// Метод для логгирования ошибок
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
/// - [exception] - исключение
/// - [stackTrace] - стек вызова
void error(String message, [Object? exception, StackTrace? stackTrace]);
/// Метод для обработки ошибок
///
/// Принимает:
/// - [error] - исключение
/// - [stackTrace] - стек вызова
/// - [message] - сообщение для логгирования в формате [String]
void handleError(Object error, [StackTrace? stackTrace, String? message]);
/// Метод для логгирования информативных сообщений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void info(String message);
/// Метод для логгирования сообщений
///
/// Принимает:
/// - [message] - сообщение для логгирования в формате [String]
void log(String message);
/// Метод для открытия окна отладки
///
/// Принимает:
/// - [context] - для определения навигатора по нему
/// - [useRootNavigator] - при true, открывает окно в корневом навигаторе
Future<T?> openDebugScreen<T>(
BuildContext context, {
bool useRootNavigator = false,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
/// {@template ErrorScreen}
/// Экран, когда в приложении произошла фатальная ошибка
/// {@endtemplate}
class ErrorScreen extends StatelessWidget {
/// {@macro ErrorScreen}
const ErrorScreen({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Что-то пошло не так, попробуйте перезагрузить приложение',
textAlign: TextAlign.center,
),
),
),
);
}
}

View File

@@ -0,0 +1,9 @@
import '../../domain/repository/i_main_repository.dart';
/// {@template MainMockRepository}
///
/// {@endtemplate}
final class MainMockRepository implements IMainRepository {
@override
String get name => 'MainMockRepository';
}

View File

@@ -0,0 +1,15 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_main_repository.dart';
/// {@template MainRepository}
///
/// {@endtemplate}
final class MainRepository implements IMainRepository {
final IHttpClient httpClient;
MainRepository({required this.httpClient});
@override
String get name => 'MainRepository';
}

View File

@@ -0,0 +1,6 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IMainRepository}
///
/// {@endtemplate}
abstract interface class IMainRepository with DiBaseRepo{}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:friflex_starter/features/main/presentation/screens/main_screen.dart';
import 'package:go_router/go_router.dart';
abstract final class MainRoutes {
/// Название роута главной страницы
static const String mainScreenName = 'main_screen';
/// Путь роута страницы профиля пользователя
static const String _mainScreenPath = '/main';
/// Метод для построения ветки роутов по фиче профиля пользователя
///
/// Принимает:
/// - [routes] - вложенные роуты
static StatefulShellBranch buildShellBranch({
List<RouteBase> routes = const [],
List<NavigatorObserver>? observers,
}) =>
StatefulShellBranch(
initialLocation: _mainScreenPath,
observers: observers,
routes: [
GoRoute(
path: _mainScreenPath,
name: mainScreenName,
builder: (context, state) => const MainScreen(),
routes: routes,
),
],
);
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 16),
Text(
'Окружение: ${context.di.appConfig.env.name}',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.theme.changeTheme();
},
child: const Text('Сменить тему'),
),
const SizedBox(height: 16),
ColoredBox(
color: context.colors.testColor,
child: const SizedBox(height: 100, width: 100),
),
const SizedBox(height: 16),
Text(
'Текущая тема: ${context.theme.themeMode}',
),
const SizedBox(height: 16),
Text(
'Текущий репозиторий: ${context.di.repositories.authRepository.name}',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('ru', 'RU'),
);
},
child: const Text('Сменить язык на Rусский'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('en', 'EN'),
);
},
child: const Text('Сменить язык на Английский'),
),
const SizedBox(height: 16),
Text(
'Тестовое слово: ${context.l10n.helloWorld}',
),
const SizedBox(height: 16),
Text(
'Текущий язык: ${context.l10n.localeName}',
),
],
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Класс для реализации корневой страницы приложения
class RootScreen extends StatelessWidget {
/// Создает корневую страницу приложения
///
/// Принимает:
/// - [navigationShell] - текущая ветка навигации
const RootScreen({
super.key,
required this.navigationShell,
});
/// Текущая ветка навигации
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.bug_report), label: 'Debug'),
],
currentIndex: navigationShell.currentIndex,
onTap: navigationShell.goBranch,
),
);
}
}

6
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,6 @@
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
}
}

6
lib/l10n/app_ru.arb Normal file
View File

@@ -0,0 +1,6 @@
{
"helloWorld": "Привет, мир!",
"@helloWorld": {
"description": "Обычное приветствие новичка-программиста"
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef LocalizationBuilder = Widget Function();
/// Виджет для перестройки виджета в зависимости от локализации
class LocalizationConsumer extends StatelessWidget {
const LocalizationConsumer({super.key, required this.builder});
final LocalizationBuilder builder;
@override
Widget build(BuildContext context) {
return Consumer<LocalizationNotifier>(
builder: (_, __, ___) {
return builder();
},
);
}
}
/// Класс для управления локализацией
final class LocalizationNotifier extends ChangeNotifier {
Locale _locale = const Locale('en', 'US');
Locale get locale => _locale;
void changeLocal(Locale locale) {
_locale = locale;
notifyListeners();
}
}

4
lib/main.dart Normal file
View File

@@ -0,0 +1,4 @@
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(AppEnv.prod).run();

View File

@@ -0,0 +1,41 @@
import 'package:flutter/cupertino.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
import 'package:friflex_starter/features/main/presentation/main_routes.dart';
import 'package:friflex_starter/features/root/root_screen.dart';
import 'package:go_router/go_router.dart';
/// Класс, реализующий роутер приложения и все поля классов
class AppRouter {
/// Конструктор для инициализации роутера
const AppRouter();
/// Ключ для доступа к корневому навигатору приложения
static final rootNavigatorKey = GlobalKey<NavigatorState>();
/// Начальный роут приложения
static String get initialLocation => '/main';
/// Метод для создания экземпляра GoRouter
static GoRouter createRouter(IDebugService debugService) {
return GoRouter(
navigatorKey: rootNavigatorKey,
debugLogDiagnostics: true,
initialLocation: initialLocation,
observers: [
debugService.createRouterObserver(),
],
routes: [
StatefulShellRoute.indexedStack(
parentNavigatorKey: rootNavigatorKey,
builder: (context, state, navigationShell) =>
RootScreen(navigationShell: navigationShell),
branches: [
MainRoutes.buildShellBranch(),
DebugRoutes.buildShellBranch(),
],
),
],
);
}
}

107
lib/runner/app_runner.dart Normal file
View File

@@ -0,0 +1,107 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:friflex_starter/app/app.dart';
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/features/debug/app_debug_service.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
import 'package:friflex_starter/features/error/error_screen.dart';
import 'package:friflex_starter/router/app_router.dart';
import 'package:friflex_starter/runner/timer_runner.dart';
import 'package:go_router/go_router.dart';
part 'errors_handlers.dart';
/// Класс, реализующий раннер для конфигурирования приложения при запуске
///
/// Порядок инициализации:
/// 1. _initApp - инициализация конфигурации приложения
/// 2. инициализация репозиториев приложения (будет позже)
/// 3. runApp - запуск приложения
/// 4. _onAppLoaded - после запуска приложения
class AppRunner {
/// Создает экземпляр раннера приложения
///
/// Принимает:
/// - [env] - тип окружения сборки приложения
AppRunner(this.env);
/// Тип окружения сборки приложения¬
final AppEnv env;
/// Контейнер зависимостей приложения
late final IDebugService _debugService;
/// Роутер приложения
late final GoRouter router;
/// Таймер для отслеживания времени инициализации приложения
late final TimerRunner _timerRunner;
/// Метод для запуска приложения
Future<void> run() async {
WidgetsFlutterBinding.ensureInitialized();
// Инициализация сервиса отладки
_debugService = AppDebugService();
_timerRunner = TimerRunner(_debugService);
// Инициализация приложения
await _initApp();
// Инициализация метода обработки ошибок
_initErrorHandlers(_debugService);
// Инициализация репозиториев и сервисов
final diContainer = await _initDependencies(_debugService);
// Инициализация роутера
router = AppRouter.createRouter(_debugService);
runApp(
App(diContainer: diContainer, router: router),
);
await _onAppLoaded();
}
/// Метод инициализации приложения,
/// выполняется до запуска приложения
Future<void> _initApp() async {
// Запрет на поворот экрана
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
// Заморозка первого кадра (сплеш)
WidgetsBinding.instance.deferFirstFrame();
}
/// Метод срабатывает после запуска приложения
Future<void> _onAppLoaded() async {
// Разморозка первого кадра (сплеш)
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.allowFirstFrame();
});
_timerRunner.stop();
}
/// Метод для инициализации зависимостей приложения
Future<DiContainer> _initDependencies(IDebugService debugService) async {
debugService.log('Тип сборки: ${env.name}');
final diContainer = DiContainer(
env: env,
dService: debugService,
);
await diContainer.init(
onProgress: _timerRunner.logOnProgress,
onComplete: _timerRunner.logOnComplete,
onError: _timerRunner.logOnError,
);
return diContainer;
}
}

View File

@@ -0,0 +1,27 @@
part of 'app_runner.dart';
/// Метод инициализации обработчиков ошибок
void _initErrorHandlers(IDebugService debugService) {
// Обработка ошибок в приложении
FlutterError.onError = (details) {
_showErrorScreen();
debugService.handleError(details.exception, details.stack,
'FlutterError.onError: ${details.exceptionAsString()}',);
};
// Обработка асинхронных ошибок в приложении
PlatformDispatcher.instance.onError = (error, stack) {
_showErrorScreen();
debugService.handleError(error, stack, 'PlatformDispatcher: $error');
return true;
};
}
/// Метод для показа экрана ошибки
void _showErrorScreen() {
WidgetsBinding.instance.addPostFrameCallback((_) {
AppRouter.rootNavigatorKey.currentState?.push(
MaterialPageRoute(
builder: (_) => const ErrorScreen(),
),
);
});
}

View File

@@ -0,0 +1,49 @@
import 'package:friflex_starter/features/debug/i_debug_service.dart';
/// {@template TimerRunner}
/// Класс для подсчета времени запуска приложения
/// {@endtemplate}
class TimerRunner {
/// {@macro TimerRunner}
TimerRunner(this._debugService) {
_stopwatch.start();
}
/// Сервис для отладки
final IDebugService _debugService;
/// Секундомер для подсчета времени инициализации
final _stopwatch = Stopwatch();
/// Метод для остановки секундомера и вывода времени
/// полной инициализации приложения
void stop() {
_stopwatch.stop();
_debugService.log(
'Время инициализации приложения: ${_stopwatch.elapsedMilliseconds} мс',
);
}
/// Метод для обработки прогресса инициализации зависимостей
void logOnProgress(String name) {
_debugService.log(
'$name успешная инициализация, прогресс: ${_stopwatch.elapsedMilliseconds} мс',
);
}
/// Метод для обработки прогресса инициализации зависимостей
void logOnComplete(String message) {
_debugService.log(
'$message, прогресс: ${_stopwatch.elapsedMilliseconds} мс',
);
}
/// Метод для обработки прогресса инициализации зависимостей
void logOnError(
String message, {
Object? error,
StackTrace? stackTrace,
}) {
_debugService.error(message, error, stackTrace);
}
}

4
lib/targets/dev.dart Normal file
View File

@@ -0,0 +1,4 @@
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(AppEnv.dev).run();

5
lib/targets/prod.dart Normal file
View File

@@ -0,0 +1,5 @@
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(AppEnv.prod).run();

5
lib/targets/stage.dart Normal file
View File

@@ -0,0 +1,5 @@
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(AppEnv.stage).run();

1018
pubspec.lock Normal file

File diff suppressed because it is too large Load Diff

85
pubspec.yaml Normal file
View File

@@ -0,0 +1,85 @@
name: friflex_starter
description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.6.0
dependencies:
flutter:
sdk: flutter
cupertino_icons: 1.0.8
envied: 1.0.1
go_router: 14.6.3
flutter_bloc: 8.1.1
provider: 6.1.2
dio: 5.7.0
intl: 0.19.0
flutter_localizations:
sdk: flutter
# Зависимости для сервиса логирования
talker_flutter: 4.6.4
talker_dio_logger: 4.6.4
talker_bloc_logger: 4.6.4
### основной сервис с интерфейсами
i_app_services:
path: ./app_services/i_app_services
app_services:
### Базовая реализация ###
path: app_services/gms/app_services
### Аврора реализация ###
#path: app_services/aurora/app_services
### Telegram реализация ###
#path: app_services/telegram/app_services
dev_dependencies:
flutter_test:
sdk: flutter
friflex_lint_rules:
hosted: https://pub.friflex.com
version: 4.0.1
envied_generator: 1.0.1
build_runner: 2.4.14
flutter:
uses-material-design: true
generate: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package