diff --git a/lec5/.gitignore b/lec5/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/lec5/.gitignore @@ -0,0 +1,43 @@ +# 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 +**/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 diff --git a/lec5/.metadata b/lec5/.metadata new file mode 100644 index 0000000..6bcf4e8 --- /dev/null +++ b/lec5/.metadata @@ -0,0 +1,33 @@ +# 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: "4cf269e36de2573851eaef3c763994f8f9be494d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + - platform: android + create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + - platform: web + create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d + + # 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' diff --git a/lec5/README.md b/lec5/README.md new file mode 100644 index 0000000..330f465 --- /dev/null +++ b/lec5/README.md @@ -0,0 +1,24 @@ +# API + +https://api.potterdb.com/v1/characters + +# Build files + +flutter pub run build_runner build --delete-conflicting-outputs + +# test_app + +Test app + +## 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. diff --git a/lec5/analysis_options.yaml b/lec5/analysis_options.yaml new file mode 100644 index 0000000..7161cfd --- /dev/null +++ b/lec5/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lec5/android/.gitignore b/lec5/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/lec5/android/.gitignore @@ -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 diff --git a/lec5/android/app/build.gradle b/lec5/android/app/build.gradle new file mode 100644 index 0000000..397a734 --- /dev/null +++ b/lec5/android/app/build.gradle @@ -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 = "ru.ulstu.is.test_app" + 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 = "ru.ulstu.is.test_app" + // 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 = "../.." +} diff --git a/lec5/android/app/src/debug/AndroidManifest.xml b/lec5/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..4d95910 --- /dev/null +++ b/lec5/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/lec5/android/app/src/main/AndroidManifest.xml b/lec5/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fdc9875 --- /dev/null +++ b/lec5/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lec5/android/app/src/main/kotlin/ru/ulstu/is/test_app/MainActivity.kt b/lec5/android/app/src/main/kotlin/ru/ulstu/is/test_app/MainActivity.kt new file mode 100644 index 0000000..60baa7b --- /dev/null +++ b/lec5/android/app/src/main/kotlin/ru/ulstu/is/test_app/MainActivity.kt @@ -0,0 +1,5 @@ +package ru.ulstu.`is`.test_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/lec5/android/app/src/main/res/drawable-v21/launch_background.xml b/lec5/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..c03a191 --- /dev/null +++ b/lec5/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/lec5/android/app/src/main/res/drawable/launch_background.xml b/lec5/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..0db4a83 --- /dev/null +++ b/lec5/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/lec5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/lec5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/lec5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/lec5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/lec5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/lec5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/lec5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/lec5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/lec5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/lec5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/lec5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/lec5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/lec5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/lec5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/lec5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/lec5/android/app/src/main/res/values-night/styles.xml b/lec5/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/lec5/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/lec5/android/app/src/main/res/values/styles.xml b/lec5/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/lec5/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/lec5/android/app/src/profile/AndroidManifest.xml b/lec5/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..4d95910 --- /dev/null +++ b/lec5/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/lec5/android/build.gradle b/lec5/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/lec5/android/build.gradle @@ -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 +} diff --git a/lec5/android/gradle.properties b/lec5/android/gradle.properties new file mode 100644 index 0000000..2597170 --- /dev/null +++ b/lec5/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/lec5/android/gradle/wrapper/gradle-wrapper.properties b/lec5/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7bb2df6 --- /dev/null +++ b/lec5/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/lec5/android/settings.gradle b/lec5/android/settings.gradle new file mode 100644 index 0000000..b9e43bd --- /dev/null +++ b/lec5/android/settings.gradle @@ -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" diff --git a/lec5/lib/data/dto/characters_dto.dart b/lec5/lib/data/dto/characters_dto.dart new file mode 100644 index 0000000..22a7421 --- /dev/null +++ b/lec5/lib/data/dto/characters_dto.dart @@ -0,0 +1,61 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'characters_dto.g.dart'; + +@JsonSerializable(createToJson: false) +class CharactersDto { + final List? data; + final MetaDto? meta; + + const CharactersDto({this.data, this.meta}); + + factory CharactersDto.fromJson(Map json) => + _$CharactersDtoFromJson(json); +} + +@JsonSerializable(createToJson: false) +class MetaDto { + final PaginationDto? pagination; + + const MetaDto({this.pagination}); + + factory MetaDto.fromJson(Map json) => + _$MetaDtoFromJson(json); +} + +@JsonSerializable(createToJson: false) +class PaginationDto { + final int? current; + final int? next; + final int? last; + + PaginationDto({this.current, this.next, this.last}); + + factory PaginationDto.fromJson(Map json) => + _$PaginationDtoFromJson(json); +} + +@JsonSerializable(createToJson: false) +class CharacterDto { + final String? id; + final String? type; + final CharacterAttributesDto? attributes; + + const CharacterDto({this.id, this.type, this.attributes}); + + factory CharacterDto.fromJson(Map json) => + _$CharacterDtoFromJson(json); +} + +@JsonSerializable(createToJson: false) +class CharacterAttributesDto { + final String? name; + final String? born; + final String? died; + final String? image; + + const CharacterAttributesDto({this.name, this.born, this.died, this.image}); + + factory CharacterAttributesDto.fromJson(Map json) => + _$CharacterAttributesDtoFromJson(json); +} diff --git a/lec5/lib/data/dto/characters_dto.g.dart b/lec5/lib/data/dto/characters_dto.g.dart new file mode 100644 index 0000000..80a973a --- /dev/null +++ b/lec5/lib/data/dto/characters_dto.g.dart @@ -0,0 +1,48 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'characters_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CharactersDto _$CharactersDtoFromJson(Map json) => + CharactersDto( + data: (json['data'] as List?) + ?.map((e) => CharacterDto.fromJson(e as Map)) + .toList(), + meta: json['meta'] == null + ? null + : MetaDto.fromJson(json['meta'] as Map), + ); + +MetaDto _$MetaDtoFromJson(Map json) => MetaDto( + pagination: json['pagination'] == null + ? null + : PaginationDto.fromJson(json['pagination'] as Map), + ); + +PaginationDto _$PaginationDtoFromJson(Map json) => + PaginationDto( + current: (json['current'] as num?)?.toInt(), + next: (json['next'] as num?)?.toInt(), + last: (json['last'] as num?)?.toInt(), + ); + +CharacterDto _$CharacterDtoFromJson(Map json) => CharacterDto( + id: json['id'] as String?, + type: json['type'] as String?, + attributes: json['attributes'] == null + ? null + : CharacterAttributesDto.fromJson( + json['attributes'] as Map), + ); + +CharacterAttributesDto _$CharacterAttributesDtoFromJson( + Map json) => + CharacterAttributesDto( + name: json['name'] as String?, + born: json['born'] as String?, + died: json['died'] as String?, + image: json['image'] as String?, + ); diff --git a/lec5/lib/data/mapper/characters_mapper.dart b/lec5/lib/data/mapper/characters_mapper.dart new file mode 100644 index 0000000..57cebe8 --- /dev/null +++ b/lec5/lib/data/mapper/characters_mapper.dart @@ -0,0 +1,36 @@ +import 'package:test_app/data/dto/characters_dto.dart'; +import 'package:test_app/domain/card.dart'; +import 'package:test_app/domain/home.dart'; + +const _imagePlaceholder = + 'https://www.bellcold.com/wp-content/uploads/2024/04/Person-Placeholder-Icon.jpeg'; + +extension CharactersDtoToModel on CharactersDto { + HomeData toDomain() => HomeData( + data: data?.map((e) => e.toDomain()).toList(), + currentPage: meta?.pagination?.current, + nextPage: meta?.pagination?.next, + ); +} + +extension CharacterDtoToModel on CharacterDto { + CardData toDomain() => CardData( + attributes?.name ?? 'UNKNOWN', + imageUrl: attributes?.image ?? _imagePlaceholder, + descriptionText: + _makeDescriptionText(attributes?.born, attributes?.died), + ); +} + +String _makeDescriptionText(String? born, String? died) { + if (born != null && died != null) { + return '$born - $died'; + } + if (born != null) { + return 'born: $born'; + } + if (died != null) { + return 'died: $died'; + } + return ''; +} diff --git a/lec5/lib/data/repository/api_interface.dart b/lec5/lib/data/repository/api_interface.dart new file mode 100644 index 0000000..0ff8c25 --- /dev/null +++ b/lec5/lib/data/repository/api_interface.dart @@ -0,0 +1,7 @@ +import 'package:test_app/domain/home.dart'; + +typedef OnErrorCallback = void Function(String? error); + +abstract class ApiInterface { + Future loadData(); +} diff --git a/lec5/lib/data/repository/mock_repository.dart b/lec5/lib/data/repository/mock_repository.dart new file mode 100644 index 0000000..3b43c92 --- /dev/null +++ b/lec5/lib/data/repository/mock_repository.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:test_app/data/repository/api_interface.dart'; +import 'package:test_app/domain/card.dart'; +import 'package:test_app/domain/home.dart'; + +class MockRepository extends ApiInterface { + @override + Future loadData() async { + return HomeData(data: [ + CardData( + 'Hello', + descriptionText: 'Hello text', + imageUrl: + 'https://unsplash.com/photos/6GMq7AGxNbE/download?ixid=M3wxMjA3fDB8MXxzZWFyY2h8Mnx8cmFjY29vbnxlbnwwfHx8fDE3Mjc5MTExNTl8MA&force=true&w=640', + ), + CardData( + 'Hello 2', + descriptionText: 'Hello 2 text', + icon: Icons.add, + imageUrl: + 'https://unsplash.com/photos/UopR2NUBYek/download?ixid=M3wxMjA3fDB8MXxzZWFyY2h8MTF8fHJhY2Nvb258ZW58MHx8fHwxNzI3OTExMTU5fDA&force=true&w=640', + ), + CardData( + 'Hello 3', + descriptionText: 'Hello 3 text', + icon: Icons.add, + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Raccoon_on_Log.jpg/640px-Raccoon_on_Log.jpg', + ), + ]); + } +} diff --git a/lec5/lib/data/repository/potter_repository.dart b/lec5/lib/data/repository/potter_repository.dart new file mode 100644 index 0000000..745237b --- /dev/null +++ b/lec5/lib/data/repository/potter_repository.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; +import 'package:test_app/data/dto/characters_dto.dart'; +import 'package:test_app/data/mapper/characters_mapper.dart'; +import 'package:test_app/data/repository/api_interface.dart'; +import 'package:test_app/domain/home.dart'; + +class PotterRepository extends ApiInterface { + static final Dio _dio = Dio() + ..interceptors.add(PrettyDioLogger( + requestHeader: true, + requestBody: false, + )); + + static const String _baseUrl = 'https://api.potterdb.com'; + + @override + Future loadData({ + String? q, + int page = 1, + int size = 10, + OnErrorCallback? onError, + }) async { + try { + const String url = '$_baseUrl/v1/characters'; + + final Response response = await _dio.get( + url, + queryParameters: { + 'filter[name_cont]': q, + 'page[number]': page, + 'page[size]': size, + }, + ); + + final CharactersDto dto = + CharactersDto.fromJson(response.data as Map); + return dto.toDomain(); + } on DioException catch (e) { + onError?.call(e.response?.statusMessage); + return null; + } + } +} diff --git a/lec5/lib/domain/card.dart b/lec5/lib/domain/card.dart new file mode 100644 index 0000000..4faa8f7 --- /dev/null +++ b/lec5/lib/domain/card.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class CardData { + final String text; + final String descriptionText; + final IconData icon; + final String? imageUrl; + + CardData( + this.text, { + required this.descriptionText, + this.icon = Icons.ac_unit_outlined, + this.imageUrl, + }); +} diff --git a/lec5/lib/domain/home.dart b/lec5/lib/domain/home.dart new file mode 100644 index 0000000..504c52b --- /dev/null +++ b/lec5/lib/domain/home.dart @@ -0,0 +1,9 @@ +import 'package:test_app/domain/card.dart'; + +class HomeData { + final List? data; + int? currentPage; + int? nextPage; + + HomeData({this.data, this.currentPage, this.nextPage}); +} diff --git a/lec5/lib/main.dart b/lec5/lib/main.dart new file mode 100644 index 0000000..6ddaf20 --- /dev/null +++ b/lec5/lib/main.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:test_app/view/home_page/home_page.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My Test App', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'My Test App'), + ); + } +} diff --git a/lec5/lib/utils/debounce.dart b/lec5/lib/utils/debounce.dart new file mode 100644 index 0000000..6e1c470 --- /dev/null +++ b/lec5/lib/utils/debounce.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'dart:ui'; + +class Debounce { + factory Debounce() => _instance; + + Debounce._(); + + static final Debounce _instance = Debounce._(); + + static Timer? _timer; + + static void run( + VoidCallback action, { + Duration delay = const Duration(milliseconds: 500), + }) { + _timer?.cancel(); + _timer = Timer(delay, action); + } +} diff --git a/lec5/lib/view/details_page/details_page.dart b/lec5/lib/view/details_page/details_page.dart new file mode 100644 index 0000000..cbaf327 --- /dev/null +++ b/lec5/lib/view/details_page/details_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:test_app/domain/card.dart'; + +class DetailsPage extends StatelessWidget { + final CardData data; + + final Color _color = Colors.orangeAccent; + + const DetailsPage(this.data, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: _color, + title: Text(data.text), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Image.network(data.imageUrl ?? ''), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + data.text, + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + Text( + data.descriptionText, + style: Theme.of(context).textTheme.headlineSmall, + ) + ], + ), + )); + } +} diff --git a/lec5/lib/view/home_page/home_card.dart b/lec5/lib/view/home_page/home_card.dart new file mode 100644 index 0000000..0323338 --- /dev/null +++ b/lec5/lib/view/home_page/home_card.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:test_app/domain/card.dart'; + +part 'home_card_image.dart'; +part 'home_card_like.dart'; +part 'home_card_text.dart'; + +typedef OnLikeCallback = void Function(String title, bool isLiked)?; + +class HomeCard extends StatelessWidget { + final String text; + final String descriptionText; + final IconData icon; + final String? imageUrl; + final OnLikeCallback onLike; + final VoidCallback? onTap; + + const HomeCard( + this.text, { + this.icon = Icons.ac_unit_outlined, + required this.descriptionText, + this.imageUrl, + this.onLike, + this.onTap, + super.key, + }); + + factory HomeCard.fromData( + CardData data, { + OnLikeCallback? onLike, + VoidCallback? onTap, + }) => + HomeCard( + data.text, + descriptionText: data.descriptionText, + icon: data.icon, + imageUrl: data.imageUrl, + onLike: onLike, + onTap: onTap, + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + constraints: const BoxConstraints(minHeight: 140), + decoration: BoxDecoration( + color: Colors.white70, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(.5), + spreadRadius: 4, + offset: const Offset(0, 5), + blurRadius: 8, + ), + ], + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _CardImage(imageUrl ?? ''), + _CardText( + text, + descriptionText: descriptionText, + ), + _CardLike( + onLike, + text: text, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lec5/lib/view/home_page/home_card_image.dart b/lec5/lib/view/home_page/home_card_image.dart new file mode 100644 index 0000000..f0b2ea6 --- /dev/null +++ b/lec5/lib/view/home_page/home_card_image.dart @@ -0,0 +1,26 @@ +part of 'home_card.dart'; + +class _CardImage extends StatelessWidget { + final String imageUrl; + + const _CardImage(this.imageUrl); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + topLeft: Radius.circular(20), + ), + child: SizedBox( + height: double.infinity, + width: 115, + child: Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + ); + } +} diff --git a/lec5/lib/view/home_page/home_card_like.dart b/lec5/lib/view/home_page/home_card_like.dart new file mode 100644 index 0000000..e632a0d --- /dev/null +++ b/lec5/lib/view/home_page/home_card_like.dart @@ -0,0 +1,50 @@ +part of 'home_card.dart'; + +class _CardLike extends StatefulWidget { + final OnLikeCallback onLike; + final String text; + + const _CardLike(this.onLike, {required this.text}); + + @override + State<_CardLike> createState() => _CardLikeState(); +} + +class _CardLikeState extends State<_CardLike> { + bool _isLiked = false; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only( + left: 8, + right: 16, + bottom: 16, + ), + child: GestureDetector( + onTap: () { + setState(() { + _isLiked = !_isLiked; + }); + widget.onLike?.call(widget.text, _isLiked); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _isLiked + ? const Icon( + Icons.favorite, + color: Colors.redAccent, + key: ValueKey(0), + ) + : const Icon( + Icons.favorite_border, + key: ValueKey(1), + ), + ), + ), + ), + ); + } +} diff --git a/lec5/lib/view/home_page/home_card_text.dart b/lec5/lib/view/home_page/home_card_text.dart new file mode 100644 index 0000000..503538e --- /dev/null +++ b/lec5/lib/view/home_page/home_card_text.dart @@ -0,0 +1,33 @@ +part of 'home_card.dart'; + +class _CardText extends StatelessWidget { + final String text; + final String descriptionText; + + const _CardText( + this.text, { + required this.descriptionText, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: Theme.of(context).textTheme.headlineLarge, + ), + Text( + descriptionText, + style: Theme.of(context).textTheme.bodyLarge, + ) + ], + ), + ), + ); + } +} diff --git a/lec5/lib/view/home_page/home_page.dart b/lec5/lib/view/home_page/home_page.dart new file mode 100644 index 0000000..07ba5d2 --- /dev/null +++ b/lec5/lib/view/home_page/home_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:test_app/data/repository/potter_repository.dart'; +import 'package:test_app/domain/card.dart'; +import 'package:test_app/domain/home.dart'; +import 'package:test_app/utils/debounce.dart'; +import 'package:test_app/view/details_page/details_page.dart'; +import 'package:test_app/view/home_page/home_card.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final Color _color = Colors.orangeAccent; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: _color, + title: Text(widget.title), + ), + body: const Body(), + ); + } +} + +class Body extends StatefulWidget { + const Body({super.key}); + + @override + State createState() => _BodyState(); +} + +class _BodyState extends State { + final searchController = TextEditingController(); + final scrollController = ScrollController(); + final repository = PotterRepository(); + int nextPage = 1; + int currentPage = 0; + List data = []; + bool isLoading = false; + + @override + void initState() { + _loadData(); + scrollController.addListener(_onNextPageListener); + super.initState(); + } + + void _setLoading(bool state) { + setState(() { + isLoading = state; + }); + } + + void _loadData() async { + _setLoading(true); + final result = + await repository.loadData(q: searchController.text, page: nextPage); + _setData(result); + _setLoading(false); + } + + void _setData(HomeData? result) { + nextPage = result?.nextPage ?? 1; + if (currentPage >= (result?.currentPage ?? 1)) { + return; + } + currentPage = result?.currentPage ?? 1; + setState(() { + data.addAll(result?.data ?? []); + }); + } + + @override + void dispose() { + searchController.dispose(); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: SearchBar( + leading: const Icon(Icons.search), + trailing: [ + IconButton( + onPressed: () { + searchController.clear(); + data.clear(); + currentPage = 0; + nextPage = 1; + _loadData(); + }, + icon: const Icon(Icons.clear)), + ], + controller: searchController, + onChanged: (search) { + Debounce.run(() async { + _setLoading(true); + data.clear(); + currentPage = 0; + nextPage = 1; + final result = await repository.loadData(q: search); + if (scrollController.hasClients) { + scrollController.jumpTo(0); + } + _setData(result); + _setLoading(false); + }); + }, + ), + ), + Expanded( + child: Stack( + children: [ + Positioned.fill( + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.only(right: 16, left: 16), + itemCount: data.length, + itemBuilder: (context, index) { + final item = data[index]; + return HomeCard.fromData( + item, + onLike: (String title, bool isLiked) => + _showSnackBar(context, title, isLiked), + onTap: () => _navigateToDetails(context, item), + ); + }), + ), + Align( + alignment: + data.isEmpty ? Alignment.topCenter : Alignment.bottomCenter, + child: Visibility( + visible: isLoading, + child: const Padding( + padding: EdgeInsets.only(bottom: 32, top: 16), + child: CircularProgressIndicator(), + ), + ), + ), + ], + ), + ), + ], + ); + } + + void _showSnackBar(BuildContext context, String title, bool isLiked) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + '$title is ${isLiked ? 'liked' : 'disliked'}!', + style: Theme.of(context).textTheme.bodyLarge, + ), + backgroundColor: Colors.orangeAccent, + duration: const Duration(seconds: 1), + )); + }); + } + + void _navigateToDetails(BuildContext context, CardData data) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => DetailsPage(data)), + ); + } + + void _onNextPageListener() { + Debounce.run(() { + if (scrollController.offset > + scrollController.position.maxScrollExtent - 10) { + _loadData(); + } + }); + } +} diff --git a/lec5/pubspec.lock b/lec5/pubspec.lock new file mode 100644 index 0000000..e872536 --- /dev/null +++ b/lec5/pubspec.lock @@ -0,0 +1,610 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.5.2 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/lec5/pubspec.yaml b/lec5/pubspec.yaml new file mode 100644 index 0000000..ae6b7ce --- /dev/null +++ b/lec5/pubspec.yaml @@ -0,0 +1,97 @@ +name: test_app +description: "Test app" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.5.2 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + # lec4 dependencies + json_annotation: ^4.9.0 + dio: ^5.7.0 + pretty_dio_logger: ^1.4.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^4.0.0 + # lec4 dependencies + build_runner: ^2.4.13 + json_serializable: ^6.8.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: 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 diff --git a/lec5/test/widget_test.dart b/lec5/test/widget_test.dart new file mode 100644 index 0000000..9b909ee --- /dev/null +++ b/lec5/test/widget_test.dart @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:test_app/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/lec5/web/favicon.png b/lec5/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/lec5/web/favicon.png differ diff --git a/lec5/web/icons/Icon-192.png b/lec5/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/lec5/web/icons/Icon-192.png differ diff --git a/lec5/web/icons/Icon-512.png b/lec5/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/lec5/web/icons/Icon-512.png differ diff --git a/lec5/web/icons/Icon-maskable-192.png b/lec5/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/lec5/web/icons/Icon-maskable-192.png differ diff --git a/lec5/web/icons/Icon-maskable-512.png b/lec5/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/lec5/web/icons/Icon-maskable-512.png differ diff --git a/lec5/web/index.html b/lec5/web/index.html new file mode 100644 index 0000000..415d762 --- /dev/null +++ b/lec5/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + test_app + + + + + + diff --git a/lec5/web/manifest.json b/lec5/web/manifest.json new file mode 100644 index 0000000..ebb86e7 --- /dev/null +++ b/lec5/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "test_app", + "short_name": "test_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Test app", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}