diff --git a/lib/animations/fade_in.dart b/lib/animations/fade_in.dart new file mode 100644 index 0000000..5387be0 --- /dev/null +++ b/lib/animations/fade_in.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:simple_animations/simple_animations.dart'; + +class FadeIn extends StatelessWidget { + final double delay; + final Widget child; + const FadeIn({Key? key, required this.child, required this.delay}) + : super(key: key); + + TimelineTween createTween() { + var tween = TimelineTween(); + var scene = tween.addScene( + duration: const Duration(milliseconds: 400), begin: Duration.zero); + scene.animate(Prop.translateX, + tween: Tween(begin: 130.0, end: 0.0), curve: Curves.easeOut); + scene.animate(Prop.opacity, tween: Tween(begin: 0.0, end: 1.0)); + return tween; + } + + @override + Widget build(BuildContext context) { + final tween = createTween(); + return PlayAnimation>( + delay: Duration(milliseconds: (200 * delay).round()), + duration: tween.duration, + builder: ((context, child, value) { + return Opacity( + opacity: value.get(Prop.opacity), + child: Transform.translate( + offset: Offset(value.get(Prop.translateX), 0), + child: child, + ), + ); + }), + tween: tween, + child: child); + } +} diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart new file mode 100644 index 0000000..a7f6a05 --- /dev/null +++ b/lib/config/get_it_config.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:todo_app/repositories/todo_repository.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; +import 'package:todo_app/use_cases/done_todo.dart'; +import 'package:todo_app/use_cases/fetch_todos.dart'; +import 'package:todo_app/use_cases/new_todo.dart'; +import 'package:todo_app/use_cases/remove_done_todos.dart'; + +extension GetItConfig on GetIt { + void init() { + registerSingleton(Dio()); + registerSingleton(TodoRepositoryImpl()); + registerSingleton(FetchTodos()); + registerSingleton(NewTodo()); + registerSingleton(DoneTodo()); + registerSingleton(RemoveDoneTodos()); + } +} diff --git a/lib/cubit/todo_list_states.dart b/lib/cubit/todo_list_states.dart new file mode 100644 index 0000000..b0cac8e --- /dev/null +++ b/lib/cubit/todo_list_states.dart @@ -0,0 +1,39 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:todo_app/models/todo.dart'; +part 'todo_list_states.freezed.dart'; + +/* @immutable +abstract class TodoListState { + List get todos => []; +} + +@freezed +class TodoListInitialState extends TodoListState with _$TodoListInitialState { + factory TodoListInitialState({required final List todos}) = + _TodoListInitialState; +} + +@unfreezed +class TodoListLoadedState extends TodoListState with _$TodoListLoadedState { + factory TodoListLoadedState({required List todos}) = + _TodoListLoadedState; +} + +@unfreezed +class TodoListFailState extends TodoListState with _$TodoListFailState { + factory TodoListFailState( + {required List todos, required String error}) = _TodoListFailState; +} + */ +@freezed +class TodoListState with _$TodoListState { + const factory TodoListState.initial( + {required List todos, required bool succes}) = Initial; + const factory TodoListState.loaded( + {required List todos, required bool succes}) = Loaded; + const factory TodoListState.fail( + {required List todos, + required bool succes, + required String error}) = Fail; +} diff --git a/lib/cubit/todo_list_states.freezed.dart b/lib/cubit/todo_list_states.freezed.dart new file mode 100644 index 0000000..9563b3d --- /dev/null +++ b/lib/cubit/todo_list_states.freezed.dart @@ -0,0 +1,625 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'todo_list_states.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$TodoListState { + List get todos => throw _privateConstructorUsedError; + bool get succes => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(List todos, bool succes) initial, + required TResult Function(List todos, bool succes) loaded, + required TResult Function(List todos, bool succes, String error) fail, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Loaded value) loaded, + required TResult Function(Fail value) fail, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $TodoListStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TodoListStateCopyWith<$Res> { + factory $TodoListStateCopyWith( + TodoListState value, $Res Function(TodoListState) then) = + _$TodoListStateCopyWithImpl<$Res>; + $Res call({List todos, bool succes}); +} + +/// @nodoc +class _$TodoListStateCopyWithImpl<$Res> + implements $TodoListStateCopyWith<$Res> { + _$TodoListStateCopyWithImpl(this._value, this._then); + + final TodoListState _value; + // ignore: unused_field + final $Res Function(TodoListState) _then; + + @override + $Res call({ + Object? todos = freezed, + Object? succes = freezed, + }) { + return _then(_value.copyWith( + todos: todos == freezed + ? _value.todos + : todos // ignore: cast_nullable_to_non_nullable + as List, + succes: succes == freezed + ? _value.succes + : succes // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +abstract class _$$InitialCopyWith<$Res> + implements $TodoListStateCopyWith<$Res> { + factory _$$InitialCopyWith(_$Initial value, $Res Function(_$Initial) then) = + __$$InitialCopyWithImpl<$Res>; + @override + $Res call({List todos, bool succes}); +} + +/// @nodoc +class __$$InitialCopyWithImpl<$Res> extends _$TodoListStateCopyWithImpl<$Res> + implements _$$InitialCopyWith<$Res> { + __$$InitialCopyWithImpl(_$Initial _value, $Res Function(_$Initial) _then) + : super(_value, (v) => _then(v as _$Initial)); + + @override + _$Initial get _value => super._value as _$Initial; + + @override + $Res call({ + Object? todos = freezed, + Object? succes = freezed, + }) { + return _then(_$Initial( + todos: todos == freezed + ? _value._todos + : todos // ignore: cast_nullable_to_non_nullable + as List, + succes: succes == freezed + ? _value.succes + : succes // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$Initial with DiagnosticableTreeMixin implements Initial { + const _$Initial({required final List todos, required this.succes}) + : _todos = todos; + + final List _todos; + @override + List get todos { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_todos); + } + + @override + final bool succes; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'TodoListState.initial(todos: $todos, succes: $succes)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'TodoListState.initial')) + ..add(DiagnosticsProperty('todos', todos)) + ..add(DiagnosticsProperty('succes', succes)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Initial && + const DeepCollectionEquality().equals(other._todos, _todos) && + const DeepCollectionEquality().equals(other.succes, succes)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_todos), + const DeepCollectionEquality().hash(succes)); + + @JsonKey(ignore: true) + @override + _$$InitialCopyWith<_$Initial> get copyWith => + __$$InitialCopyWithImpl<_$Initial>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(List todos, bool succes) initial, + required TResult Function(List todos, bool succes) loaded, + required TResult Function(List todos, bool succes, String error) fail, + }) { + return initial(todos, succes); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + }) { + return initial?.call(todos, succes); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + required TResult orElse(), + }) { + if (initial != null) { + return initial(todos, succes); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Loaded value) loaded, + required TResult Function(Fail value) fail, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class Initial implements TodoListState { + const factory Initial( + {required final List todos, + required final bool succes}) = _$Initial; + + @override + List get todos => throw _privateConstructorUsedError; + @override + bool get succes => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$InitialCopyWith<_$Initial> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LoadedCopyWith<$Res> implements $TodoListStateCopyWith<$Res> { + factory _$$LoadedCopyWith(_$Loaded value, $Res Function(_$Loaded) then) = + __$$LoadedCopyWithImpl<$Res>; + @override + $Res call({List todos, bool succes}); +} + +/// @nodoc +class __$$LoadedCopyWithImpl<$Res> extends _$TodoListStateCopyWithImpl<$Res> + implements _$$LoadedCopyWith<$Res> { + __$$LoadedCopyWithImpl(_$Loaded _value, $Res Function(_$Loaded) _then) + : super(_value, (v) => _then(v as _$Loaded)); + + @override + _$Loaded get _value => super._value as _$Loaded; + + @override + $Res call({ + Object? todos = freezed, + Object? succes = freezed, + }) { + return _then(_$Loaded( + todos: todos == freezed + ? _value._todos + : todos // ignore: cast_nullable_to_non_nullable + as List, + succes: succes == freezed + ? _value.succes + : succes // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$Loaded with DiagnosticableTreeMixin implements Loaded { + const _$Loaded({required final List todos, required this.succes}) + : _todos = todos; + + final List _todos; + @override + List get todos { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_todos); + } + + @override + final bool succes; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'TodoListState.loaded(todos: $todos, succes: $succes)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'TodoListState.loaded')) + ..add(DiagnosticsProperty('todos', todos)) + ..add(DiagnosticsProperty('succes', succes)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Loaded && + const DeepCollectionEquality().equals(other._todos, _todos) && + const DeepCollectionEquality().equals(other.succes, succes)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_todos), + const DeepCollectionEquality().hash(succes)); + + @JsonKey(ignore: true) + @override + _$$LoadedCopyWith<_$Loaded> get copyWith => + __$$LoadedCopyWithImpl<_$Loaded>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(List todos, bool succes) initial, + required TResult Function(List todos, bool succes) loaded, + required TResult Function(List todos, bool succes, String error) fail, + }) { + return loaded(todos, succes); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + }) { + return loaded?.call(todos, succes); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(todos, succes); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Loaded value) loaded, + required TResult Function(Fail value) fail, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class Loaded implements TodoListState { + const factory Loaded( + {required final List todos, required final bool succes}) = _$Loaded; + + @override + List get todos => throw _privateConstructorUsedError; + @override + bool get succes => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$LoadedCopyWith<_$Loaded> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$FailCopyWith<$Res> implements $TodoListStateCopyWith<$Res> { + factory _$$FailCopyWith(_$Fail value, $Res Function(_$Fail) then) = + __$$FailCopyWithImpl<$Res>; + @override + $Res call({List todos, bool succes, String error}); +} + +/// @nodoc +class __$$FailCopyWithImpl<$Res> extends _$TodoListStateCopyWithImpl<$Res> + implements _$$FailCopyWith<$Res> { + __$$FailCopyWithImpl(_$Fail _value, $Res Function(_$Fail) _then) + : super(_value, (v) => _then(v as _$Fail)); + + @override + _$Fail get _value => super._value as _$Fail; + + @override + $Res call({ + Object? todos = freezed, + Object? succes = freezed, + Object? error = freezed, + }) { + return _then(_$Fail( + todos: todos == freezed + ? _value._todos + : todos // ignore: cast_nullable_to_non_nullable + as List, + succes: succes == freezed + ? _value.succes + : succes // ignore: cast_nullable_to_non_nullable + as bool, + error: error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$Fail with DiagnosticableTreeMixin implements Fail { + const _$Fail( + {required final List todos, + required this.succes, + required this.error}) + : _todos = todos; + + final List _todos; + @override + List get todos { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_todos); + } + + @override + final bool succes; + @override + final String error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'TodoListState.fail(todos: $todos, succes: $succes, error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'TodoListState.fail')) + ..add(DiagnosticsProperty('todos', todos)) + ..add(DiagnosticsProperty('succes', succes)) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Fail && + const DeepCollectionEquality().equals(other._todos, _todos) && + const DeepCollectionEquality().equals(other.succes, succes) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_todos), + const DeepCollectionEquality().hash(succes), + const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + _$$FailCopyWith<_$Fail> get copyWith => + __$$FailCopyWithImpl<_$Fail>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(List todos, bool succes) initial, + required TResult Function(List todos, bool succes) loaded, + required TResult Function(List todos, bool succes, String error) fail, + }) { + return fail(todos, succes, error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + }) { + return fail?.call(todos, succes, error); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(List todos, bool succes)? initial, + TResult Function(List todos, bool succes)? loaded, + TResult Function(List todos, bool succes, String error)? fail, + required TResult orElse(), + }) { + if (fail != null) { + return fail(todos, succes, error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Initial value) initial, + required TResult Function(Loaded value) loaded, + required TResult Function(Fail value) fail, + }) { + return fail(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + }) { + return fail?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Initial value)? initial, + TResult Function(Loaded value)? loaded, + TResult Function(Fail value)? fail, + required TResult orElse(), + }) { + if (fail != null) { + return fail(this); + } + return orElse(); + } +} + +abstract class Fail implements TodoListState { + const factory Fail( + {required final List todos, + required final bool succes, + required final String error}) = _$Fail; + + @override + List get todos => throw _privateConstructorUsedError; + @override + bool get succes => throw _privateConstructorUsedError; + String get error => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$FailCopyWith<_$Fail> get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/cubit/todolist_cubit.dart b/lib/cubit/todolist_cubit.dart new file mode 100644 index 0000000..0d05289 --- /dev/null +++ b/lib/cubit/todolist_cubit.dart @@ -0,0 +1,53 @@ +import 'package:bloc/bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:todo_app/cubit/todo_list_states.dart'; +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/use_cases/done_todo.dart'; +import 'package:todo_app/use_cases/fetch_todos.dart'; +import 'package:todo_app/use_cases/new_todo.dart'; +import 'package:todo_app/use_cases/remove_done_todos.dart'; + +import 'package:todo_app/models/todo.dart'; + +class TodolistCubit extends Cubit { + TodolistCubit() : super(const TodoListState.initial(todos: [], succes: true)); + + void fetchTodos() async { + final Result result = await GetIt.instance().call(); + + return result.isSuccess + ? emit(TodoListState.loaded(todos: result.data, succes: true)) + : emit(TodoListState.fail( + todos: state.todos, error: result.error as String, succes: false)); + } + + void addTodo(Todo todo) async { + final Result result = await GetIt.instance().call(todo: todo); + return result.isSuccess + ? emit(TodoListState.loaded( + todos: [...state.todos, result.data], succes: true)) + : emit(TodoListState.fail( + todos: state.todos, error: result.error as String, succes: false)); + } + + void updateTodo(int index, bool? value) async { + final Result result = + await GetIt.instance().call(id: state.todos[index].id); + return result.isSuccess + ? emit(TodoListState.loaded(todos: result.data, succes: true)) + : emit(TodoListState.fail( + todos: state.todos, error: result.error as String, succes: false)); + } + + void clearDone() async { + List todosId = state.todos.map((element) { + if (element.done == true) return element.id; + }).toList(); + final Result result = + await GetIt.instance().call(todosId: todosId); + return result.isSuccess + ? emit(TodoListState.loaded(todos: result.data, succes: true)) + : emit(TodoListState.fail( + todos: state.todos, error: result.error as String, succes: false)); + } +} diff --git a/lib/extensions/build_context_extensions.dart b/lib/extensions/build_context_extensions.dart index 2dc72e0..54f4c74 100644 --- a/lib/extensions/build_context_extensions.dart +++ b/lib/extensions/build_context_extensions.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; extension ArgumentExtractor on BuildContext { Object? get arguments => ModalRoute.of(this)!.settings.arguments; } + +extension CubitExtension on BuildContext { + T cubit>() => BlocProvider.of(this); +} diff --git a/lib/main.dart b/lib/main.dart index 785daac..ad27101 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:todo_app/cubit/todolist_cubit.dart'; import 'package:todo_app/views/home_page.dart'; +import 'package:todo_app/config/get_it_config.dart'; void main(List args) { + GetIt.instance.init(); runApp(const TodoApp()); } @@ -10,9 +15,12 @@ class TodoApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - title: 'Todo App', - home: HomePage(), + return BlocProvider( + create: (context) => TodolistCubit()..fetchTodos(), + child: const MaterialApp( + title: 'Todo App', + home: HomePage(), + ), ); } } diff --git a/lib/models/result.dart b/lib/models/result.dart new file mode 100644 index 0000000..2534bfc --- /dev/null +++ b/lib/models/result.dart @@ -0,0 +1,142 @@ +import 'package:equatable/equatable.dart'; +// Code: https://gist.githubusercontent.com/CassiusPacheco/409e66e220ce563440df00385f39ac98/raw/d0506e4b3dadbcf5a21d9cc23b300ecbcc8c57d6/data_result.dart + +/// This abstraction contains either a success data of generic type `S` or a +/// failure error of type `Failure` as its result. +/// +/// `data` property must only be retrieved when `DataResult` was constructed by +/// using `DataResult.success(value)`. It can be validated by calling +/// `isSuccess` first. Alternatively, `dataOrElse` can be used instead since it +/// ensures a valid value is returned in case the result is a failure. +/// +/// `error` must only be retrieved when `DataResult` was constructed by using +/// `DataResult.failure(error)`. It can be validated by calling `isFailure` +/// first. +abstract class Result extends Equatable { + static Result failure(Object failure) => _FailureResult(failure); + + static Result success(S data) => _SuccessResult(data); + + static Future> fromFuture( + Future Function() computation) async { + try { + return Result.success(await computation()); + } catch (e) { + return Result.failure(e); + } + } + + // ignore: prefer_const_constructors_in_immutables + Result._(); + + factory Result(S Function() computation) { + try { + return Result.success(computation()); + } catch (e) { + return Result.failure(e); + } + } + + /// Get [error] value, returns null when the value is actually [data] + Object? get error => fold((error) => error, (data) => null); + + /// Get [data] value, returns null when the value is actually [error] + S? get data => fold((error) => null, (data) => data); + + /// Returns `true` if the object is of the `SuccessResult` type, which means + /// `data` will return a valid result. + bool get isSuccess => this is _SuccessResult; + + /// Returns `true` if the object is of the `FailureResult` type, which means + /// `error` will return a valid result. + bool get isFailure => this is _FailureResult; + + /// Returns `data` if `isSuccess` returns `true`, otherwise it returns + /// `other`. + S dataOrElse(S other) => isSuccess && data != null ? data! : other; + + /// Sugar syntax that calls `dataOrElse` under the hood. Returns left value if + /// `isSuccess` returns `true`, otherwise it returns the right value. + S operator |(S other) => dataOrElse(other); + + /// Transforms values of [error] and [data] in new a `DataResult` type. Only + /// the matching function to the object type will be executed. For example, + /// for a `SuccessResult` object only the [fnData] function will be executed. + Result either( + Function(Object error) fnFailure, T Function(S data) fnData); + + /// Transforms value of [data] allowing a new `DataResult` to be returned. + /// A `SuccessResult` might return a `FailureResult` and vice versa. + Result then(Result Function(S data) fnData); + + /// Transforms value of [data] always keeping the original identity of the + /// `DataResult`, meaning that a `SuccessResult` returns a `SuccessResult` and + /// a `FailureResult` always returns a `FailureResult`. + Result map(T Function(S data) fnData); + + /// Folds [error] and [data] into the value of one type. Only the matching + /// function to the object type will be executed. For example, for a + /// `SuccessResult` object only the [fnData] function will be executed. + T fold(T Function(Object error) fnFailure, T Function(S data) fnData); + + @override + List get props => [if (isSuccess) data else error]; +} + +/// Success implementation of `DataResult`. It contains `data`. It's abstracted +/// away by `DataResult`. It shouldn't be used directly in the app. +class _SuccessResult extends Result { + final S _value; + + _SuccessResult(this._value) : super._(); + + @override + _SuccessResult either( + Function(Object error) fnFailure, T Function(S data) fnData) { + return _SuccessResult(fnData(_value)); + } + + @override + Result then(Result Function(S data) fnData) { + return fnData(_value); + } + + @override + _SuccessResult map(T Function(S data) fnData) { + return _SuccessResult(fnData(_value)); + } + + @override + T fold(T Function(Object error) fnFailure, T Function(S data) fnData) { + return fnData(_value); + } +} + +/// Failure implementation of `DataResult`. It contains `error`. It's +/// abstracted away by `DataResult`. It shouldn't be used directly in the app. +class _FailureResult extends Result { + final Object _value; + + _FailureResult(this._value) : super._(); + + @override + _FailureResult either( + Function(Object error) fnFailure, T Function(S data) fnData) { + return _FailureResult(fnFailure(_value)); + } + + @override + _FailureResult map(T Function(S data) fnData) { + return _FailureResult(_value); + } + + @override + _FailureResult then(Result Function(S data) fnData) { + return _FailureResult(_value); + } + + @override + T fold(T Function(Object error) fnFailure, T Function(S data) fnData) { + return fnFailure(_value); + } +} diff --git a/lib/models/todo.dart b/lib/models/todo.dart index ee26c7c..641da73 100644 --- a/lib/models/todo.dart +++ b/lib/models/todo.dart @@ -2,12 +2,20 @@ import 'dart:core'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'todo.freezed.dart'; +part 'todo.g.dart'; +@JsonSerializable() @unfreezed class Todo with _$Todo { factory Todo({ required final String title, - required final String description, + required final String id, + // ignore: invalid_annotation_target + @JsonKey(name: 'text') required final String description, @Default(false) bool done, }) = _Todo; + + factory Todo.fromJson(Map json) => _$TodoFromJson(json); + @override + Map toJson() => _$TodoToJson(this); } diff --git a/lib/models/todo.freezed.dart b/lib/models/todo.freezed.dart index d9ebfcd..9689940 100644 --- a/lib/models/todo.freezed.dart +++ b/lib/models/todo.freezed.dart @@ -14,13 +14,21 @@ T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); +Todo _$TodoFromJson(Map json) { + return _Todo.fromJson(json); +} + /// @nodoc mixin _$Todo { String get title => throw _privateConstructorUsedError; + String get id => + throw _privateConstructorUsedError; // ignore: invalid_annotation_target + @JsonKey(name: 'text') String get description => throw _privateConstructorUsedError; bool get done => throw _privateConstructorUsedError; set done(bool value) => throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $TodoCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -29,7 +37,11 @@ mixin _$Todo { abstract class $TodoCopyWith<$Res> { factory $TodoCopyWith(Todo value, $Res Function(Todo) then) = _$TodoCopyWithImpl<$Res>; - $Res call({String title, String description, bool done}); + $Res call( + {String title, + String id, + @JsonKey(name: 'text') String description, + bool done}); } /// @nodoc @@ -43,6 +55,7 @@ class _$TodoCopyWithImpl<$Res> implements $TodoCopyWith<$Res> { @override $Res call({ Object? title = freezed, + Object? id = freezed, Object? description = freezed, Object? done = freezed, }) { @@ -51,6 +64,10 @@ class _$TodoCopyWithImpl<$Res> implements $TodoCopyWith<$Res> { ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, description: description == freezed ? _value.description : description // ignore: cast_nullable_to_non_nullable @@ -68,7 +85,11 @@ abstract class _$$_TodoCopyWith<$Res> implements $TodoCopyWith<$Res> { factory _$$_TodoCopyWith(_$_Todo value, $Res Function(_$_Todo) then) = __$$_TodoCopyWithImpl<$Res>; @override - $Res call({String title, String description, bool done}); + $Res call( + {String title, + String id, + @JsonKey(name: 'text') String description, + bool done}); } /// @nodoc @@ -83,6 +104,7 @@ class __$$_TodoCopyWithImpl<$Res> extends _$TodoCopyWithImpl<$Res> @override $Res call({ Object? title = freezed, + Object? id = freezed, Object? description = freezed, Object? done = freezed, }) { @@ -91,6 +113,10 @@ class __$$_TodoCopyWithImpl<$Res> extends _$TodoCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, description: description == freezed ? _value.description : description // ignore: cast_nullable_to_non_nullable @@ -104,13 +130,23 @@ class __$$_TodoCopyWithImpl<$Res> extends _$TodoCopyWithImpl<$Res> } /// @nodoc - +@JsonSerializable() class _$_Todo with DiagnosticableTreeMixin implements _Todo { - _$_Todo({required this.title, required this.description, this.done = false}); + _$_Todo( + {required this.title, + required this.id, + @JsonKey(name: 'text') required this.description, + this.done = false}); + + factory _$_Todo.fromJson(Map json) => _$$_TodoFromJson(json); @override final String title; @override + final String id; +// ignore: invalid_annotation_target + @override + @JsonKey(name: 'text') final String description; @override @JsonKey() @@ -118,7 +154,7 @@ class _$_Todo with DiagnosticableTreeMixin implements _Todo { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'Todo(title: $title, description: $description, done: $done)'; + return 'Todo(title: $title, id: $id, description: $description, done: $done)'; } @override @@ -127,6 +163,7 @@ class _$_Todo with DiagnosticableTreeMixin implements _Todo { properties ..add(DiagnosticsProperty('type', 'Todo')) ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('id', id)) ..add(DiagnosticsProperty('description', description)) ..add(DiagnosticsProperty('done', done)); } @@ -135,17 +172,28 @@ class _$_Todo with DiagnosticableTreeMixin implements _Todo { @override _$$_TodoCopyWith<_$_Todo> get copyWith => __$$_TodoCopyWithImpl<_$_Todo>(this, _$identity); + + @override + Map toJson() { + return _$$_TodoToJson(this); + } } abstract class _Todo implements Todo { factory _Todo( {required final String title, - required final String description, + required final String id, + @JsonKey(name: 'text') required final String description, bool done}) = _$_Todo; + factory _Todo.fromJson(Map json) = _$_Todo.fromJson; + @override String get title => throw _privateConstructorUsedError; @override + String get id => throw _privateConstructorUsedError; + @override // ignore: invalid_annotation_target + @JsonKey(name: 'text') String get description => throw _privateConstructorUsedError; @override bool get done => throw _privateConstructorUsedError; diff --git a/lib/models/todo.g.dart b/lib/models/todo.g.dart new file mode 100644 index 0000000..c288706 --- /dev/null +++ b/lib/models/todo.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'todo.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$TodoToJson(Todo instance) => { + 'title': instance.title, + 'id': instance.id, + 'text': instance.description, + 'done': instance.done, + }; + +_$_Todo _$$_TodoFromJson(Map json) => _$_Todo( + title: json['title'] as String, + id: json['id'] as String, + description: json['text'] as String, + done: json['done'] as bool? ?? false, + ); + +Map _$$_TodoToJson(_$_Todo instance) => { + 'title': instance.title, + 'id': instance.id, + 'text': instance.description, + 'done': instance.done, + }; diff --git a/lib/repositories/todo_repository.dart b/lib/repositories/todo_repository.dart new file mode 100644 index 0000000..0fe0c4c --- /dev/null +++ b/lib/repositories/todo_repository.dart @@ -0,0 +1,58 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/models/todo.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; + +class TodoRepositoryImpl implements TodoRepository { + final Dio _dio = GetIt.instance(); + + final String _url = 'http://localhost:3005/api'; + + TodoRepositoryImpl(); + + @override + Future>> getTodos() async { + try { + var response = await _dio.get('$_url/notes'); + return Result.success( + response.data.map((todo) => Todo.fromJson(todo)).toList()); + } on DioError catch (e) { + return Result.failure(e.message); + } + } + + @override + Future> addTodo({required Todo todo}) async { + try { + var response = await _dio.post('$_url/notes', data: todo.toJson()); + return Result.success(Todo.fromJson(response.data)); + } on DioError catch (e) { + return Result.failure(e.message); + } + } + + @override + Future>> updateTodo({required String todoId}) async { + var url = '$_url/note/$todoId'; + try { + var response = await _dio.put(url); + return Result.success( + response.data.map((todo) => Todo.fromJson(todo)).toList()); + } on DioError catch (e) { + return Result.failure(e.message); + } + } + + @override + Future>> removeTodos( + {required List todosId}) async { + try { + var response = await _dio.put('$_url/notes', data: todosId); + return Result.success( + response.data.map((todo) => Todo.fromJson(todo)).toList()); + } on DioError catch (e) { + return Result.failure(e.message); + } + } +} diff --git a/lib/repositories/todo_repository_interface.dart b/lib/repositories/todo_repository_interface.dart new file mode 100644 index 0000000..caed239 --- /dev/null +++ b/lib/repositories/todo_repository_interface.dart @@ -0,0 +1,10 @@ +import 'package:todo_app/models/result.dart'; + +import 'package:todo_app/models/todo.dart'; + +abstract class TodoRepository { + Future>> getTodos(); + Future> addTodo({required Todo todo}); + Future>> updateTodo({required String todoId}); + Future>> removeTodos({required List todosId}); +} diff --git a/lib/use_cases/done_todo.dart b/lib/use_cases/done_todo.dart new file mode 100644 index 0000000..9dc3067 --- /dev/null +++ b/lib/use_cases/done_todo.dart @@ -0,0 +1,12 @@ +import 'package:get_it/get_it.dart'; +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; + +class DoneTodo { + DoneTodo(); + + final _repository = GetIt.instance(); + + Future call({required String id}) async => + await _repository.updateTodo(todoId: id); +} diff --git a/lib/use_cases/fetch_todos.dart b/lib/use_cases/fetch_todos.dart new file mode 100644 index 0000000..3dd62f3 --- /dev/null +++ b/lib/use_cases/fetch_todos.dart @@ -0,0 +1,10 @@ +import 'package:get_it/get_it.dart'; +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; + +class FetchTodos { + FetchTodos(); + final _repository = GetIt.instance(); + + Future call() async => await _repository.getTodos(); +} diff --git a/lib/use_cases/new_todo.dart b/lib/use_cases/new_todo.dart new file mode 100644 index 0000000..bd54b5c --- /dev/null +++ b/lib/use_cases/new_todo.dart @@ -0,0 +1,13 @@ +import 'package:get_it/get_it.dart'; + +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/models/todo.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; + +class NewTodo { + NewTodo(); + final _repository = GetIt.instance(); + + Future call({required Todo todo}) async => + await _repository.addTodo(todo: todo); +} diff --git a/lib/use_cases/remove_done_todos.dart b/lib/use_cases/remove_done_todos.dart new file mode 100644 index 0000000..aa7014d --- /dev/null +++ b/lib/use_cases/remove_done_todos.dart @@ -0,0 +1,11 @@ +import 'package:get_it/get_it.dart'; +import 'package:todo_app/models/result.dart'; +import 'package:todo_app/repositories/todo_repository_interface.dart'; + +class RemoveDoneTodos { + RemoveDoneTodos(); + final _repository = GetIt.instance(); + + Future call({required List todosId}) async => + await _repository.removeTodos(todosId: todosId); +} diff --git a/lib/views/detailed_todo_page.dart b/lib/views/detailed_todo_page.dart index 71f3aa1..1dc1aaf 100644 --- a/lib/views/detailed_todo_page.dart +++ b/lib/views/detailed_todo_page.dart @@ -1,27 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:todo_app/cubit/todolist_cubit.dart'; import 'package:todo_app/extensions/build_context_extensions.dart'; -import 'package:todo_app/models/todo.dart'; +import 'package:todo_app/cubit/todo_list_states.dart'; -class DetaledTodoPage extends StatefulWidget { +class DetaledTodoPage extends StatelessWidget { const DetaledTodoPage({Key? key}) : super(key: key); - @override - State createState() => _DetaledTodoPageState(); -} - -class _DetaledTodoPageState extends State { @override Widget build(BuildContext context) { - final todo = context.arguments as Todo; + final index = context.arguments as int; return Scaffold( + backgroundColor: Colors.grey[100], appBar: AppBar( title: const Text('Details'), leading: Row( children: [ TextButton.icon( - onPressed: () => Navigator.pop(context, todo), + onPressed: () => Navigator.pop(context), icon: const Icon( Icons.navigate_before, color: Colors.white, @@ -38,51 +36,85 @@ class _DetaledTodoPageState extends State { ), leadingWidth: 100, ), - body: Container( - padding: const EdgeInsets.all(20), - color: Colors.white, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - todo.done ? 'Done' : 'Not done', - style: const TextStyle( - color: Color.fromARGB(255, 244, 2, 164), - fontWeight: FontWeight.w500), - ), - Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - todo.title, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 30, + body: BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsets.all(20), + color: Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AnimatedCrossFade( + firstCurve: Curves.easeInOut, + secondCurve: Curves.easeIn, + crossFadeState: state.todos[index].done + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 300), + firstChild: const Text( + 'Done', + style: TextStyle( + color: Color.fromARGB(255, 244, 2, 164), + fontWeight: FontWeight.w500), + ), + secondChild: const Text( + 'Not done', + style: TextStyle( + color: Color.fromARGB(255, 244, 2, 164), + fontWeight: FontWeight.w500), + ), ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 10), - child: Text(todo.description), - ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 20), - child: TextButton( - onPressed: () => setState(() { - todo.done = !todo.done; - }), + Padding( + padding: const EdgeInsets.only(top: 10), child: Text( - todo.done ? 'MARK AS NOT DONE' : 'MARK AS DONE', + state.todos[index].title, style: const TextStyle( - color: Color.fromARGB(255, 244, 2, 164), - fontWeight: FontWeight.w600), + fontWeight: FontWeight.w600, + fontSize: 30, + ), ), ), - ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text(state.todos[index].description), + ), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: TextButton( + onPressed: () { + context + .cubit() + .updateTodo(index, !state.todos[index].done); + }, + child: AnimatedCrossFade( + firstCurve: Curves.easeInOut, + secondCurve: Curves.easeIn, + crossFadeState: !state.todos[index].done + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 200), + firstChild: const Text( + 'MARK AS DONE', + style: TextStyle( + color: Color.fromARGB(255, 244, 2, 164), + fontWeight: FontWeight.w600), + ), + secondChild: const Text( + 'MARK AS NOT DONE', + style: TextStyle( + color: Color.fromARGB(255, 244, 2, 164), + fontWeight: FontWeight.w600), + ), + ), + ), + ), + ), + ], ), - ], - ), + ); + }, ), ); } diff --git a/lib/views/error_view.dart b/lib/views/error_view.dart new file mode 100644 index 0000000..facc252 --- /dev/null +++ b/lib/views/error_view.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +SnackBar errorMessageView(String error) => SnackBar( + behavior: SnackBarBehavior.floating, + content: Text('Opps... Error: $error Ocurred'), + ); diff --git a/lib/views/home_page.dart b/lib/views/home_page.dart index a9c6e2d..5b4e40b 100644 --- a/lib/views/home_page.dart +++ b/lib/views/home_page.dart @@ -8,6 +8,7 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.grey[200], appBar: AppBar( title: const Text('Todo'), centerTitle: true, @@ -28,10 +29,7 @@ class HomePage extends StatelessWidget { ) ], ), - body: Container( - color: Colors.grey[200], - child: const ListViewPage(), - ), + body: const ListViewPage(), ); } } diff --git a/lib/views/list_view_page.dart b/lib/views/list_view_page.dart index f483acc..8353410 100644 --- a/lib/views/list_view_page.dart +++ b/lib/views/list_view_page.dart @@ -1,91 +1,126 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:todo_app/animations/fade_in.dart'; +import 'package:todo_app/cubit/todolist_cubit.dart'; +import 'package:todo_app/extensions/build_context_extensions.dart'; import 'package:todo_app/views/detailed_todo_page.dart'; +import 'package:todo_app/views/error_view.dart'; -import '../models/todo.dart'; +import 'package:todo_app/cubit/todo_list_states.dart'; +import 'package:todo_app/models/todo.dart'; -class ListViewPage extends StatefulWidget { +class ListViewPage extends StatelessWidget { const ListViewPage({Key? key}) : super(key: key); - @override - _ListViewPageState createState() => _ListViewPageState(); -} - -class _ListViewPageState extends State { - final _todoList = [ - Todo(title: 'Two-line item', description: 'description'), - Todo(title: 'Two-line item', description: 'description'), - Todo(title: 'Two-line item', description: 'description'), - ]; + Widget build(BuildContext context) { + final cubit = context.cubit(); + return BlocConsumer( + listener: (context, state) { + if (!state.succes) { + String error = state.maybeWhen( + fail: ((todos, succes, error) => error), orElse: () => ''); + ScaffoldMessenger.of(context).showSnackBar(errorMessageView(error)); + } + }, + builder: (context, state) { + return Container( + color: Colors.white, + child: state.todos.isNotEmpty + ? todolistTiles(state.todos, cubit) + : emtpyTodoList(), + ); + }, + ); + } - void handleCheckbox(bool? newValue, int index) => setState( - () { - _todoList[index].done = newValue!; - }, - ); + SnackBar errorMessage(String error) { + return SnackBar( + behavior: SnackBarBehavior.floating, + content: Text(error), + action: SnackBarAction( + label: 'Action', + onPressed: () {}, + ), + ); + } - @override - Widget build(BuildContext context) { + Container emtpyTodoList() { return Container( - color: Colors.white, - child: ListView.separated( - itemBuilder: ((context, index) { - if (index == _todoList.length) return const ClearDoneTodos(); + color: Colors.grey[200], + child: const Center( + child: Text( + "You don't have any todos", + style: TextStyle( + color: Color.fromARGB(255, 244, 2, 164), + fontWeight: FontWeight.w600, + fontSize: 17, + ), + ), + ), + ); + } - return ListTile( - title: Text(_todoList[index].title), - subtitle: const Text('Secondary text'), - trailing: Checkbox( - value: _todoList[index].done, - onChanged: (bool? newValue) => handleCheckbox(newValue, index), - checkColor: Colors.white, - activeColor: const Color.fromARGB(255, 244, 2, 164), - ), - onTap: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DetaledTodoPage(), - settings: RouteSettings( - arguments: _todoList[index], + ListView todolistTiles(List state, TodolistCubit cubit) => + ListView.separated( + itemBuilder: ((context, index) { + return (index == state.length) + ? ClearDoneTodos( + handleClearDone: cubit.clearDone, + ) + : FadeIn( + delay: 1.0, + child: ListTile( + //tileColor: Colors.white, + key: ObjectKey(state[index].id), + title: Text(state[index].title), + subtitle: Text(state[index].title), + trailing: Checkbox( + value: state[index].done, + onChanged: (bool? newValue) => + cubit.updateTodo(index, newValue), + checkColor: Colors.white, + activeColor: const Color.fromARGB(255, 244, 2, 164), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DetaledTodoPage(), + settings: RouteSettings( + arguments: index, + ), + ), + ); + }, ), - ), - ); - updateListTile(index, result); - }, - ); + ); }), separatorBuilder: (BuildContext context, int index) { - if (index == _todoList.length - 1) { - return const Divider( - color: Colors.transparent, - ); - } - return const Divider(); + return (index == state.length - 1) + ? const Divider( + color: Colors.transparent, + ) + : const Divider(); }, - itemCount: _todoList.length + 1, + itemCount: state.length + 1, shrinkWrap: true, - ), - ); - } - - void updateListTile(int index, result) { - return setState(() { - _todoList[index] = result; - }); - } + ); } class ClearDoneTodos extends StatelessWidget { const ClearDoneTodos({ Key? key, + this.handleClearDone, }) : super(key: key); + final VoidCallback? handleClearDone; + @override Widget build(BuildContext context) { return Container( - color: const Color.fromARGB(108, 243, 243, 243), + color: Colors.grey[200], child: TextButton( - onPressed: () {}, + onPressed: handleClearDone, child: const Text( 'CLEAR ALL DONE', style: TextStyle( diff --git a/lib/views/new_todo_page.dart b/lib/views/new_todo_page.dart index 5ade489..5c3182f 100644 --- a/lib/views/new_todo_page.dart +++ b/lib/views/new_todo_page.dart @@ -1,20 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class NewTodoPage extends StatelessWidget { +import 'package:todo_app/cubit/todolist_cubit.dart'; +import 'package:todo_app/extensions/build_context_extensions.dart'; +import 'package:todo_app/models/todo.dart'; + +class NewTodoPage extends StatefulWidget { const NewTodoPage({Key? key}) : super(key: key); + @override + State createState() => _NewTodoPageState(); +} + +class _NewTodoPageState extends State { + final TextEditingController tittleController = TextEditingController(); + + final TextEditingController descriptionController = TextEditingController(); + + final bloc = BlocProvider; + @override Widget build(BuildContext context) { + final cubit = context.cubit(); return Scaffold( appBar: AppBar( actions: [ TextButton( - style: TextButton.styleFrom( - textStyle: const TextStyle( - fontSize: 18, fontWeight: FontWeight.w400), - primary: Colors.white), - onPressed: () => {Navigator.pop(context)}, - child: const Text('Save')) + style: TextButton.styleFrom( + textStyle: + const TextStyle(fontSize: 18, fontWeight: FontWeight.w400), + primary: Colors.white), + onPressed: () { + Todo item = Todo( + id: '', + title: tittleController.text, + description: descriptionController.text); + cubit.addTodo(item); + Navigator.pop( + context, + item, + ); + }, + child: const Text('Save'), + ) ], leading: TextButton( style: TextButton.styleFrom( @@ -29,42 +57,46 @@ class NewTodoPage extends StatelessWidget { ), body: Container( color: Colors.grey[200], - child: Column(children: [ - Container( - padding: const EdgeInsets.all(20), - color: Colors.white, - child: Column( - children: [ - Transform.scale( - scale: 1.05, - child: TextFormField( - decoration: const InputDecoration( - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + color: Colors.white, + child: Column( + children: [ + Transform.scale( + scale: 1.05, + child: TextFormField( + controller: tittleController, + decoration: const InputDecoration( + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Color.fromARGB(255, 244, 2, 164), + width: 2.0), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( color: Color.fromARGB(255, 244, 2, 164), - width: 2.0), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 244, 2, 164), + ), ), + labelText: 'Task title', + labelStyle: TextStyle(color: Colors.grey, fontSize: 30), ), - labelText: 'Task title', - labelStyle: TextStyle(color: Colors.grey, fontSize: 30), ), ), - ), - TextFormField( - decoration: const InputDecoration( - border: InputBorder.none, - labelText: 'Task description', - labelStyle: TextStyle(color: Colors.grey), + TextFormField( + controller: descriptionController, + decoration: const InputDecoration( + border: InputBorder.none, + labelText: 'Task description', + labelStyle: TextStyle(color: Colors.grey), + ), ), - ), - ], + ], + ), ), - ), - ]), + ], + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 4d383a3..eecc898 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.3" boolean_selector: dependency: transitive description: @@ -162,6 +169,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" fake_async: dependency: transitive description: @@ -188,6 +209,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.0" flutter_lints: dependency: "direct dev" description: @@ -221,6 +249,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + get_it: + dependency: "direct main" + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" glob: dependency: transitive description: @@ -264,12 +299,19 @@ packages: source: hosted version: "0.6.4" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation url: "https://pub.dartlang.org" source: hosted version: "4.5.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "6.2.0" lints: dependency: transitive description: @@ -312,6 +354,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -333,6 +382,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" pub_semver: dependency: transitive description: @@ -361,6 +417,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + simple_animations: + dependency: "direct main" + description: + name: simple_animations + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" sky_engine: dependency: transitive description: flutter @@ -373,6 +436,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" source_span: dependency: transitive description: @@ -466,3 +536,4 @@ packages: version: "3.1.1" sdks: dart: ">=2.17.1 <3.0.0" + flutter: ">=1.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index ca76455..a7594a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,12 +29,20 @@ environment: dependencies: flutter: sdk: flutter + + flutter_bloc: 8.0.0 + + dio: 4.0.6 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: 1.0.2 freezed_annotation: 2.0.3 + json_annotation: 4.5.0 + equatable: 2.0.3 + simple_animations: 4.1.0 + get_it: 7.2.0 dev_dependencies: flutter_test: @@ -48,6 +56,7 @@ dev_dependencies: flutter_lints: 2.0.0 build_runner: 2.1.11 freezed: 2.0.3+1 + json_serializable: 6.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec