diff --git a/lib/BaseStatelessWidget.dart b/lib/BaseStatelessWidget.dart new file mode 100644 index 0000000..462dd9d --- /dev/null +++ b/lib/BaseStatelessWidget.dart @@ -0,0 +1,54 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import 'BaseViewModel.dart'; +import 'Utils/Constants.dart'; +import 'Utils/ViewState.dart'; +import 'Utils/ViewUtils.dart'; + +abstract class BaseStatelessWidget + extends StatelessWidget { + const BaseStatelessWidget({super.key}); + + T createViewModel(); + + Widget displayWidget(BuildContext context, T model, Widget? child); + + @override + Widget build(BuildContext parent) { + return Scaffold( + body: Container( + padding: const EdgeInsets.all(PADDING_GLOBAL), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [colourTwo, colourThree])), + child: ViewModelBuilder.reactive( + viewModelBuilder: () => createViewModel(), + builder: (context, model, child) { + var state = model.viewState; + + if (state is HasStarted) { + onStarted(); + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is HasError) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ViewUtils.displayToast(parent, state.error)); + } + return Center( + child: displayWidget(context, model, child), + ); + }, + onModelReady: (T model) => onModelReady(model)), + ), + ); + } + + void onModelReady(model) {} + + void onStarted() {} +} diff --git a/lib/BaseViewModel.dart b/lib/BaseViewModel.dart new file mode 100644 index 0000000..0a09c6d --- /dev/null +++ b/lib/BaseViewModel.dart @@ -0,0 +1,24 @@ +import 'package:stacked/stacked.dart'; + +import 'Utils/ViewState.dart'; + +abstract class BaseViewmodel extends BaseViewModel{ + + ViewState _viewState = Idle(); + ViewState get viewState => _viewState; + + void onStart() { + _viewState = HasStarted(); + notifyListeners(); + } + + void onSuccess(dynamic data) { + _viewState = HasData(data); + notifyListeners(); + } + + void onError(String error) { + _viewState = HasError(error); + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/Home.dart b/lib/Home.dart new file mode 100644 index 0000000..e4d1d87 --- /dev/null +++ b/lib/Home.dart @@ -0,0 +1,43 @@ +import 'package:easy_cc_flutter/MainViewModel.dart'; +import 'package:easy_cc_flutter/views/DropDownBox.dart'; +import 'package:easy_cc_flutter/views/EditText.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; + +import 'BaseStatelessWidget.dart'; + +class HomePage extends BaseStatelessWidget { + @override + MainViewModel createViewModel() { + return MainViewModel(); + } + + @override + Widget displayWidget( + BuildContext context, MainViewModel model, Widget? child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + DropDownBox(model.data, (selected) {}), + EditText("Enter conversion from", model.top, (selection) => {}) + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + DropDownBox(model.data, (selected) {}), + EditText("Enter conversion from", model.bottom, (selection) => {}) + ], + ), + ), + ], + ); + } +} diff --git a/lib/MainViewModel.dart b/lib/MainViewModel.dart new file mode 100644 index 0000000..0f0c776 --- /dev/null +++ b/lib/MainViewModel.dart @@ -0,0 +1,163 @@ +import 'package:easy_cc_flutter/BaseViewModel.dart'; + +class MainViewModel extends BaseViewmodel { + + String? top; + String? bottom; + + final List data = ['ALL - Albanian Lek', + 'AFN - Afghan Afghani', + 'DZD - Algerian Dinar', + 'AOA - Angolan Kwanza', + 'ARS - Argentine Peso', + 'AMD - Armenian Dram', + 'AWG - Aruban Florin', + 'AUD - Australian Dollar', + 'AZN - Azerbaijani Manat', + 'BSD - Bahamian Dollar', + 'BHD - Bahraini Dinar', + 'BDT - Bangladeshi Taka', + 'BBD - Barbadian Dollar', + 'BYR - Belarusian Ruble', + 'BZD - Belize Dollar', + 'BTN - Bhutanese Ngultrum', + 'BTC - Bitcoin', + 'BOB - Bolivian Boliviano', + 'BAM - Bosnia And Herzegovina Konvertibilna Marka', + 'BWP - Botswana Pula', + 'BRL - Brazilian Real', + 'GBP - British Pound', + 'BND - Brunei Dollar', + 'BGN - Bulgarian Lev', + 'BIF - Burundi Franc', + 'KHR - Cambodian Riel', + 'CAD - Canadian Dollar', + 'CVE - Cape Verdean Escudo', + 'KYD - Cayman Islands Dollar', + 'XAF - Central African CFA Franc', + 'XPF - CFP Franc', + 'CLP - Chilean Peso', + 'CNY - Chinese Yuan', + 'COP - Colombian Peso', + 'KMF - Comorian Franc', + 'CDF - Congolese Franc', + 'CRC - Costa Rican Colon', + 'HRK - Croatian Kuna', + 'CUP - Cuban Peso', + 'CZK - Czech Koruna', + 'DKK - Danish Krone', + 'DJF - Djiboutian Franc', + 'DOP - Dominican Peso', + 'XCD - East Caribbean Dollar', + 'EGP - Egyptian Pound', + 'ERN - Eritrean Nakfa', + 'ETB - Ethiopian Birr', + 'EUR - Euro', + 'FKP - Falkland Islands Pound', + 'FJD - Fijian Dollar', + 'GMD - Gambian Dalasi', + 'GEL - Georgian Lari', + 'GHS - Ghanaian Cedi', + 'GIP - Gibraltar Pound', + 'GTQ - Guatemalan Quetzal', + 'GNF - Guinean Franc', + 'GYD - Guyanese Dollar', + 'HTG - Haitian Gourde', + 'HNL - Honduran Lempira', + 'HKD - Hong Kong Dollar', + 'HUF - Hungarian Forint', + 'ISK - Icelandic Kr\u00f3na', + 'INR - Indian Rupee', + 'IDR - Indonesian Rupiah', + 'IRR - Iranian Rial', + 'IQD - Iraqi Dinar', + 'ILS - Israeli New Sheqel', + 'JMD - Jamaican Dollar', + 'JPY - Japanese Yen', + 'JOD - Jordanian Dinar', + 'KZT - Kazakhstani Tenge', + 'KES - Kenyan Shilling', + 'KWD - Kuwaiti Dinar', + 'KGS - Kyrgyzstani Som', + 'LAK - Lao Kip', + 'LVL - Latvian Lats', + 'LBP - Lebanese Lira', + 'LSL - Lesotho Loti', + 'LRD - Liberian Dollar', + 'LYD - Libyan Dinar', + 'MOP - Macanese Pataca', + 'MKD - Macedonian Denar', + 'MGA - Malagasy Ariary', + 'MWK - Malawian Kwacha', + 'MYR - Malaysian Ringgit', + 'MVR - Maldivian Rufiyaa', + 'MRO - Mauritanian Ouguiya', + 'MUR - Mauritian Rupee', + 'MXN - Mexican Peso', + 'MDL - Moldovan Leu', + 'MNT - Mongolian Tugrik', + 'MAD - Moroccan Dirham', + 'MZN - Mozambican Metical', + 'MMK - Myanma Kyat', + 'NAD - Namibian Dollar', + 'NPR - Nepalese Rupee', + 'ANG - Netherlands Antillean Gulden', + 'TWD - New Taiwan Dollar', + 'NZD - New Zealand Dollar', + 'NIO - Nicaraguan Cordoba', + 'NGN - Nigerian Naira', + 'KPW - North Korean Won', + 'NOK - Norwegian Krone', + 'OMR - Omani Rial', + 'TOP - Paanga', + 'PKR - Pakistani Rupee', + 'PAB - Panamanian Balboa', + 'PGK - Papua New Guinean Kina', + 'PYG - Paraguayan Guarani', + 'PEN - Peruvian Nuevo Sol', + 'PHP - Philippine Peso', + 'PLN - Polish Zloty', + 'QAR - Qatari Riyal', + 'RON - Romanian Leu', + 'RUB - Russian Ruble', + 'RWF - Rwandan Franc', + 'SHP - Saint Helena Pound', + 'WST - Samoan Tala', + 'STD - Sao Tome And Principe Dobra', + 'SAR - Saudi Riyal', + 'RSD - Serbian Dinar', + 'SCR - Seychellois Rupee', + 'SLL - Sierra Leonean Leone', + 'SGD - Singapore Dollar', + 'SBD - Solomon Islands Dollar', + 'SOS - Somali Shilling', + 'ZAR - South African Rand', + 'KRW - South Korean Won', + 'XDR - Special Drawing Rights', + 'LKR - Sri Lankan Rupee', + 'SDG - Sudanese Pound', + 'SRD - Surinamese Dollar', + 'SZL - Swazi Lilangeni', + 'SEK - Swedish Krona', + 'CHF - Swiss Franc', + 'SYP - Syrian Pound', + 'TJS - Tajikistani Somoni', + 'TZS - Tanzanian Shilling', + 'THB - Thai Baht', + 'TTD - Trinidad and Tobago Dollar', + 'TND - Tunisian Dinar', + 'TRY - Turkish New Lira', + 'TMT - Turkmenistan Manat', + 'AED - UAE Dirham', + 'UGX - Ugandan Shilling', + 'UAH - Ukrainian Hryvnia', + 'USD - United States Dollar', + 'UYU - Uruguayan Peso', + 'UZS - Uzbekistani Som', + 'VUV - Vanuatu Vatu', + 'VEF - Venezuelan Bolivar', + 'VND - Vietnamese Dong', + 'XOF - West African CFA Franc', + 'YER - Yemeni Rial', + 'ZMW - Zambian Kwacha']; +} \ No newline at end of file diff --git a/lib/Utils/Constants.dart b/lib/Utils/Constants.dart new file mode 100644 index 0000000..b937973 --- /dev/null +++ b/lib/Utils/Constants.dart @@ -0,0 +1,9 @@ +import 'package:flutter/cupertino.dart'; + +const Color colourOne = Color(0xFF253031); +const Color colourTwo = Color(0xFF315659); +const Color colourThree = Color(0xFF2978A0); +const Color colourFour = Color(0xFF8549ff); +const Color colourFive = Color(0xFFC6E0FF); + +const double PADDING_GLOBAL = 12; \ No newline at end of file diff --git a/lib/Utils/ViewState.dart b/lib/Utils/ViewState.dart new file mode 100644 index 0000000..fd6e37f --- /dev/null +++ b/lib/Utils/ViewState.dart @@ -0,0 +1,20 @@ +import 'package:sealed_annotations/sealed_annotations.dart'; + +@Sealed() +abstract class ViewState {} + +class Idle implements ViewState {} + +class HasStarted implements ViewState {} + +class HasData implements ViewState { + final dynamic data; + + HasData(this.data); +} + +class HasError implements ViewState { + final String error; + + HasError(this.error); +} diff --git a/lib/Utils/ViewUtils.dart b/lib/Utils/ViewUtils.dart new file mode 100644 index 0000000..8bd66dd --- /dev/null +++ b/lib/Utils/ViewUtils.dart @@ -0,0 +1,18 @@ +import 'package:flutter/cupertino.dart'; +import 'package:toast/toast.dart'; + +class ViewUtils{ + + static displayToast(BuildContext context, String message){ + Toast.show( + message, + duration: Toast.lengthLong, + gravity: Toast.bottom + ); + } + + static displayToastDeferred(BuildContext context, String message){ + WidgetsBinding.instance.addPostFrameCallback( + (_) => displayToast(context, message)); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e016029..05fdff5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'Home.dart'; + void main() { runApp(const MyApp()); } @@ -24,92 +26,7 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + home: HomePage(), ); } } diff --git a/lib/views/DropDownBox.dart b/lib/views/DropDownBox.dart new file mode 100644 index 0000000..c6f9e15 --- /dev/null +++ b/lib/views/DropDownBox.dart @@ -0,0 +1,37 @@ +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../Utils/Constants.dart'; + +class DropDownBox extends StatelessWidget { + final List _selection; + final Function(String?) _onChanged; + + const DropDownBox(this._selection, this._onChanged, {super.key}); + + @override + Widget build(BuildContext context) { + return Card( + color: colourThree, + elevation: 2, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(22))), + child: DropdownSearch( + popupProps: const PopupProps.dialog(showSearchBox: true), + items: _selection, + dropdownDecoratorProps: const DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + contentPadding: EdgeInsets.all(14), + border: InputBorder.none, + filled: true, + fillColor: Colors.transparent, + hintText: "Select a currency", + ), + ), + onChanged: _onChanged, + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/EditText.dart b/lib/views/EditText.dart new file mode 100644 index 0000000..05108cb --- /dev/null +++ b/lib/views/EditText.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../Utils/Constants.dart'; + +class EditText extends StatelessWidget { + final String _hintText; + final String? _input; + final Function(String?) _onChanged; + + const EditText(this._hintText, this._input, this._onChanged, {super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + child: TextField( + controller: TextEditingController( + text: _input + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(14), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(22)), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: colourTwo, + hintText: _hintText, + ), + onChanged: _onChanged, + + ), + ); + } + +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 5d37b7c..8c5f4f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: @@ -43,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -67,6 +88,39 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + get: + dependency: transitive + description: + name: get + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.5" + get_it: + dependency: transitive + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + lazy_evaluation: + dependency: "direct main" + description: + name: lazy_evaluation + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" lints: dependency: transitive description: @@ -95,6 +149,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -102,6 +163,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" + sealed_annotations: + dependency: "direct main" + description: + name: sealed_annotations + url: "https://pub.dartlang.org" + source: hosted + version: "1.13.0" sky_engine: dependency: transitive description: flutter @@ -121,6 +196,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0" + stacked: + dependency: "direct main" + description: + name: stacked + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.15" + stacked_core: + dependency: transitive + description: + name: stacked_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + stacked_services: + dependency: "direct main" + description: + name: stacked_services + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.26" stream_channel: dependency: transitive description: @@ -149,6 +245,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + toast: + dependency: "direct main" + description: + name: toast + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + validators: + dependency: "direct main" + description: + name: validators + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" vector_math: dependency: transitive description: @@ -158,3 +282,4 @@ packages: version: "2.1.2" sdks: dart: ">=2.18.0-190.0.dev <3.0.0" + flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index f71a5ac..6592fe7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,15 @@ environment: dependencies: flutter: sdk: flutter - + dropdown_search: 5.0.0 + # utils + toast: ^0.3.0 + validators: ^3.0.0 + lazy_evaluation: ^1.1.0 + sealed_annotations: ^1.13.0 + # stacked + stacked: ^2.1.9 + stacked_services: ^0.8.13 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.