diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d8abe1b9..1726d93d 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.13.9", - "flavors": {} + "flutterSdkVersion": "3.13.9" } \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d875..7040cb04 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4a..2951c6d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/versions/3.13.9", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/assets/mock_restaurants.json b/assets/mock_restaurants.json new file mode 100644 index 00000000..a8f1bcf9 --- /dev/null +++ b/assets/mock_restaurants.json @@ -0,0 +1,837 @@ +{ + "total": 6251, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "VzJIMZRW-8lwoFJzk0jAXw", + "rating": 5, + "user": { + "id": "i2dS47auJ-9-OW4xZSPxAA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/M2AsmeEgwVwpjyaE1lFtIA/o.jpg", + "name": "White R." + } + }, + { + "id": "H85bnGMvTx0ACssHvyCyug", + "rating": 5, + "user": { + "id": "3xfzp3cOhKICnLn0D9ZheA", + "image_url": null, + "name": "Molly S." + } + }, + { + "id": "O60EsMnhEmrSs4F54imSkw", + "rating": 3, + "user": { + "id": "C8e7rVhQY6lMYm-yn1luJQ", + "image_url": null, + "name": "Taylor T." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "uIeZrx9X1W0XPKqDicXZew", + "rating": 5, + "user": { + "id": "nvcvPpKYpq-nT7wwAexGYw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/2_pHFKGZ3-SlBq_HTXp8wg/o.jpg", + "name": "Tanner D." + } + }, + { + "id": "V8KFADRFJnsGUvQ3iRtnig", + "rating": 5, + "user": { + "id": "R_PPnsl0gsIvzhq9JHRCXQ", + "image_url": null, + "name": "Misha Z." + } + }, + { + "id": "NTi315CS824pvOsnqZmnww", + "rating": 5, + "user": { + "id": "swpNJGPBG4XCiMH7FeOUZg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/Tft5UPZtZ4zMNtVgdvbIwQ/o.jpg", + "name": "Kimberly Z." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "59ewmBp3j19Ud3T7Lz4-Ow", + "rating": 4, + "user": { + "id": "7DQSAc84ydnYEP2UMWG0oQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/xV_gdMBDART7WwHp8KPlkA/o.jpg", + "name": "Shannon P." + } + }, + { + "id": "tcrSG4NQUQktQNTncUnA8A", + "rating": 5, + "user": { + "id": "8AFvV4hG3IsoG7kyDXRZLw", + "image_url": null, + "name": "Lonfre A." + } + }, + { + "id": "SLncTZbrWzvn4QMiOb1brA", + "rating": 5, + "user": { + "id": "RB_lfittmnIRVL-m-4Q5YQ", + "image_url": null, + "name": "Suanne K." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "CfkhNXxjrmAG3Rqth2S8PA", + "rating": 5, + "user": { + "id": "Lh3eh4seBFvfTgybYXL1uw", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/oztzeYdcy2lE6edfoXhapQ/o.jpg", + "name": "Christine X." + } + }, + { + "id": "BVXWtpN-Xn4eIjMe2eceQg", + "rating": 5, + "user": { + "id": "azjuxjks4tfg_HCt1D5zqQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/dD5Mtong507Zkt91BLKLNQ/o.jpg", + "name": "Lexie P." + } + }, + { + "id": "OqxFOKP3qvm3jnnZJaxOUw", + "rating": 5, + "user": { + "id": "-DG-God4RyXPOsaGmPNDcg", + "image_url": null, + "name": "Leilani S." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "lXHzKzJfUaMHh3d6L4CcVw", + "rating": 3, + "user": { + "id": "owWvMQ5g6ZokQKyrKHptug", + "image_url": null, + "name": "Big B." + } + }, + { + "id": "ZRJNeoKFkVnjWFv7uiUBSg", + "rating": 5, + "user": { + "id": "VtXtUlnnTX78cMQrQJYfJQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/gpd9sy4O0ng7HOvfsyn5tA/o.jpg", + "name": "Montse C." + } + }, + { + "id": "31ss_mS7gZ-n87m9WtiO4Q", + "rating": 5, + "user": { + "id": "moPMSTcAmuXV1gGOmrSxCA", + "image_url": null, + "name": "Samuel B." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "vT4P__nHekuPUyMaX84iDg", + "rating": 5, + "user": { + "id": "Em4JJUrZwNQmEcOKcgsTdA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/Z-JwlzFO03ZPg1Ipmhfq6A/o.jpg", + "name": "Sara K." + } + }, + { + "id": "pzndfUZHiKN9udZXpZP3GA", + "rating": 4, + "user": { + "id": "rjV-cGARjLT5NCXE4QoITQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/csz_4XilwyqvCgY7-VzSpA/o.jpg", + "name": "Eric C." + } + }, + { + "id": "UspFMU3KmguqGMihIkH5jA", + "rating": 5, + "user": { + "id": "gYgFW1ZF603FuEbqgUEvzw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/CxRlXjvcv7Icovk1QIFSiQ/o.jpg", + "name": "Elaine L." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "CsuamYmDAWthyn6AxCvXZQ", + "rating": 5, + "user": { + "id": "BHIGJAgBTRD4H01yP0n9Qg", + "image_url": null, + "name": "Genesis S." + } + }, + { + "id": "MlbzJT2UhcebcoXtq0kczA", + "rating": 5, + "user": { + "id": "HVwq3FtsOuxQV9DvoPa4RA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/qptBQ1iGo9kY9wDGF8bFHA/o.jpg", + "name": "Jasmine B." + } + }, + { + "id": "0AG_FlfYBfxlEIQKepSktw", + "rating": 5, + "user": { + "id": "Cco2owhvdzfZ-UAS4tPwKw", + "image_url": null, + "name": "Audrey D." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "XvYKeYfYU2mDODBphOlYXA", + "rating": 4, + "user": { + "id": "Vz5KVB8oq1Yd-yBjjUzq7w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/H-8KWUKW8ujADR3a-LW3wA/o.jpg", + "name": "Teresa L." + } + }, + { + "id": "j3qyXxcVl1N5o01AuCbJkw", + "rating": 5, + "user": { + "id": "VBIGHWAj0lhxHtqEjNgXOw", + "image_url": null, + "name": "Gary G." + } + }, + { + "id": "aigNWful677P85ArYZRkqw", + "rating": 5, + "user": { + "id": "yhZ-fJdtaImuUL-lW6e9-g", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/1sU9tinpG1SuG4e5h9EE3g/o.jpg", + "name": "Nicole T." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "CbF8xDxRzLfCmuDRC6tfPw", + "rating": 5, + "user": { + "id": "iv9NN0Vwy3hH-BKVyM_t-g", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/Irz4u42GVEDRvHJHY4z6Hg/o.jpg", + "name": "Nancy L." + } + }, + { + "id": "2zno95s1q0S55ZnQD-3RHQ", + "rating": 4, + "user": { + "id": "epPsJevS8chv-tvxYDCSew", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/mh4egjwst_25_ChKKjlSBA/o.jpg", + "name": "Dianna H." + } + }, + { + "id": "KpmUpAaEq--1tyGVIOxSwA", + "rating": 5, + "user": { + "id": "a2pHeokH8l7r3T8nXach0A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/2LLnjIvr0y9yMen135cJKQ/o.jpg", + "name": "Kam M." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "igHYkXZMLAc9UdV5VnR_AA", + "name": "Echo & Rig", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/Q9swks1BO-w-hVskIHrCVg/o.jpg" + ], + "reviews": [ + { + "id": "KxB6EqbsUAYcXCbogF0j9A", + "rating": 5, + "user": { + "id": "W3opz1HpIXl2krFLJ53lqg", + "image_url": null, + "name": "Ming Z." + } + }, + { + "id": "to14TViy1ksheDKAfHkQ1Q", + "rating": 5, + "user": { + "id": "fe4LgCw7X9TZCocwvr-LTQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/JN2tU8aDVk68I3ywHepdkg/o.jpg", + "name": "Shane B." + } + }, + { + "id": "N4t02F-XrnuY_Zf2-tQk1Q", + "rating": 4, + "user": { + "id": "xz81pPXEuon4-7yRg2ptDQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/HWfZOqm3rqN8r0QY-4ORzQ/o.jpg", + "name": "Grace C." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Butcher", + "alias": "butcher" + }, + { + "title": "Tapas/Small Plates", + "alias": "tapasmallplates" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "440 S Rampart Blvd\nLas Vegas, NV 89145" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "nSRTIxL_xl5m8gykOEq-WQ", + "rating": 5, + "user": { + "id": "zaERxTbPn4TW6f6jynAb_A", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/A7Cov8r811C0tZiVArEn3A/o.jpg", + "name": "Kayleene M." + } + }, + { + "id": "IUYj8Lox9pD1pda-a2rF1w", + "rating": 5, + "user": { + "id": "8ExBbLTrT8Gc1R9r4KhBCg", + "image_url": null, + "name": "Angelica M." + } + }, + { + "id": "2rcy8s1va17xnmQut-qkOw", + "rating": 5, + "user": { + "id": "8h9-semB4gW931Q5B1LJ4w", + "image_url": null, + "name": "mike m." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "b0Vc0DyssvQ6eR9zVI2bnQ", + "rating": 5, + "user": { + "id": "rky5o20jykKRbssb4aoo7g", + "image_url": null, + "name": "doyle W." + } + }, + { + "id": "rFkhUcAd_toiQF5etzOUFw", + "rating": 5, + "user": { + "id": "TgVjm7u8yWeP7E8E8HLi9w", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mpciVuTs9rLYXIS2T9Jszg/o.jpg", + "name": "Jose P." + } + }, + { + "id": "VwdNtA6ZVYH2CQ2S8_8V7A", + "rating": 3, + "user": { + "id": "j-g4mLPahUwSNzWzKIIQuA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/-qUjr9ZJN6lDaGEHjcXLlA/o.jpg", + "name": "Kelly M." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "rsFWnc3wXsDeFXa2ATFZiQ", + "rating": 5, + "user": { + "id": "L6TIKUDGuzGRkfvP56-tKA", + "image_url": null, + "name": "Deneen M." + } + }, + { + "id": "bPYqNEvJaQxkAeZDmXRCug", + "rating": 5, + "user": { + "id": "4G8bg7wdu_WHezo1c1EYgA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jzQtjP49LXCkTH9ANt04KQ/o.jpg", + "name": "Rabindra A." + } + }, + { + "id": "I-CloBpIZZhvpFBaQTOs7g", + "rating": 3, + "user": { + "id": "G9x-ugGo1HI7J2SItKUzMg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/WRHViSHCN_TvnuaM-LF_wA/o.jpg", + "name": "Michelle T." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + ], + "reviews": [ + { + "id": "o6bSMlXb1PBUZPZFQYFxWA", + "rating": 5, + "user": { + "id": "sqgULvS3Y3PkZHWaEXDUsw", + "image_url": null, + "name": "Laurie C." + } + }, + { + "id": "Yio6qxUapbvBWmR2HX-Q7g", + "rating": 5, + "user": { + "id": "edZ6kTxithgtctw1crIkBA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/vgHMPe8zIBtiVbQ_sEXjpw/o.jpg", + "name": "Liza Faye T." + } + }, + { + "id": "7e3O21G7nc3fgtXqaunwMA", + "rating": 5, + "user": { + "id": "Fp3ahB16xguTAi9FqtSZtw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/LqFLe9gQBvS_6PwZNrtWNQ/o.jpg", + "name": "Julie N." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + } + ] +} \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..7e7e7f67 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..f68de6f2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + #target 'RunnerTests' do + #inherit! :search_paths + #end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..a41b4536 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,37 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + +PODFILE CHECKSUM: 692bace833b660596368f5e5903a29181eef0f24 + +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..7af7c5c1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2878E0C2E481F24C270AE513 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB52062EC8C59B596765BF37 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -32,9 +33,11 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5299DCE3EBCE76FBF4DA87D8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 87A197ED848EA95F3C0230A5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB52062EC8C59B596765BF37 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B67B84F5FACC1335F8251268 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2878E0C2E481F24C270AE513 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0280117EEE41C5E203769462 /* Pods */ = { + isa = PBXGroup; + children = ( + 87A197ED848EA95F3C0230A5 /* Pods-Runner.debug.xcconfig */, + 5299DCE3EBCE76FBF4DA87D8 /* Pods-Runner.release.xcconfig */, + B67B84F5FACC1335F8251268 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 646BF599FA2E713798490FE8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AB52062EC8C59B596765BF37 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +97,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 0280117EEE41C5E203769462 /* Pods */, + 646BF599FA2E713798490FE8 /* Frameworks */, ); sourceTree = ""; }; @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 97CDAE6BF738374ADB54110E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + CA18C7AE0E75570573B61109 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -200,6 +229,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 97CDAE6BF738374ADB54110E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CA18C7AE0E75570573B61109 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -275,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -352,7 +420,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/common/constants.dart b/lib/common/constants.dart new file mode 100644 index 00000000..b9779a06 --- /dev/null +++ b/lib/common/constants.dart @@ -0,0 +1,4 @@ +const kEmptyString = ""; +const kZeroInt = 0; +const kZeroDouble = 0.0; +const kFavoriteRestaurantsKey = "kFavoriteRestaurantsKey"; diff --git a/lib/common/extensions.dart b/lib/common/extensions.dart new file mode 100644 index 00000000..6fc3d67f --- /dev/null +++ b/lib/common/extensions.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +extension Theme1 on BuildContext { + ThemeData get theme { + return Theme.of(this); + } +} \ No newline at end of file diff --git a/lib/custom_widget/custom_app_bar.dart b/lib/custom_widget/custom_app_bar.dart new file mode 100644 index 00000000..0ddf4765 --- /dev/null +++ b/lib/custom_widget/custom_app_bar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/common/extensions.dart'; + +AppBar getCustomAppBar( + BuildContext context, + String? title, { + List? actions, + bool forceMaterialTransparency = true, + Widget? leading, + PreferredSizeWidget? bottom, + Alignment? titleAlignment, + }) { + return AppBar( + leading: leading, + title: Align( + alignment: titleAlignment ?? Alignment.centerLeft, + child: Text( + title ?? kEmptyString, + style: context.theme.textTheme.headlineSmall?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + forceMaterialTransparency: forceMaterialTransparency, + actions: actions, + bottom: bottom, + ); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..a4e754c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/screens/tabs/tabs_screen.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); runApp(const Restaurantour()); } @@ -14,7 +15,21 @@ class Restaurantour extends StatelessWidget { return MaterialApp( title: 'RestauranTour', theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, + // colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), + useMaterial3: true, + appBarTheme: const AppBarTheme(backgroundColor: Colors.white), + navigationBarTheme: + const NavigationBarThemeData(backgroundColor: Colors.white), + floatingActionButtonTheme: + const FloatingActionButtonThemeData(backgroundColor: Colors.grey), + scaffoldBackgroundColor: Colors.white, + + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.green, + secondary: Colors.grey, + primary: Colors.green, + onPrimary: Colors.black87, + ), ), home: const HomePage(), ); @@ -26,32 +41,6 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); + return const TabsScreen(); } } diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index f251d7b4..f7ecae55 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,8 +1,14 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:restaurantour/common/constants.dart'; import 'package:restaurantour/models/restaurant.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -const _apiKey = ''; +const _apiKey = + 'T738X0O1WDOKjwt42uLksMj4P2U0AAPnawfy4ASjPi46IJgXdXAU3kPjoFMKp1p0bor4jpsN7upFVYzCyNPPiHWT6c2i8wA0y7yS92Rk5vrRSa9XeJHBMGq1nHUfZnYx'; class YelpRepository { late Dio dio; @@ -16,10 +22,15 @@ class YelpRepository { headers: { 'Authorization': 'Bearer $_apiKey', 'Content-Type': 'application/graphql', + 'accept': 'application/json' }, ), ); + void setDio(Dio dio) { + this.dio = dio; + } + /// Returns a response in this shape /// { /// "data": { @@ -64,16 +75,38 @@ class YelpRepository { '/v3/graphql', data: _getQuery(offset), ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); + return RestaurantQueryResult.fromJson(response.data!); + } catch (e) { + return Future.error(e); + } + } + + Future getRestaurantsMocked({int offset = 0}) async { + try { + final String jsonString = + await rootBundle.loadString('assets/mock_restaurants.json'); + final data = jsonDecode(jsonString); + print(data); + return RestaurantQueryResult.fromJson(data); } catch (e) { - return null; + return Future.error(e); } } + Future> getFavoriteRestaurantsIds() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(kFavoriteRestaurantsKey) ?? []; + } + + Future setFavoriteRestaurantsIds(List restaurantIds) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setStringList(kFavoriteRestaurantsKey, restaurantIds); + } + String _getQuery(int offset) { return ''' query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { + search(location: "Las Vegas", limit: 14, offset: $offset) { total business { id @@ -81,7 +114,7 @@ query getRestaurants { price rating photos - reviews { + reviews(limit: 3) { id rating user { diff --git a/lib/screens/restaurant_details/restaurant_details_screen.dart b/lib/screens/restaurant_details/restaurant_details_screen.dart new file mode 100644 index 00000000..3a48668c --- /dev/null +++ b/lib/screens/restaurant_details/restaurant_details_screen.dart @@ -0,0 +1,274 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/common/extensions.dart'; +import 'package:restaurantour/custom_widget/custom_app_bar.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/screens/restaurant_details/restaurant_details_vm.dart'; +import 'package:restaurantour/screens/restaurant_details/review_item.dart'; + +class RestaurantDetails extends StatelessWidget { + const RestaurantDetails({ + super.key, + required this.restaurant, + }); + + final Restaurant restaurant; + + static Future push(BuildContext context, Restaurant restaurant) { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RestaurantDetails( + restaurant: restaurant, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => RestaurantDetailsVM(), + child: Consumer( + builder: (context, vm, _) { + vm.init(restaurant); + + return Scaffold( + appBar: getCustomAppBar( + context, + restaurant.name, + actions: [ + FutureBuilder( + future: vm.isFavorite(), + builder: (context, snapshot) { + return InkWell( + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + snapshot.data == false + ? Icons.favorite_border + : Icons.favorite, + size: 28, + color: Colors.black, + ), + ), + borderRadius: BorderRadius.circular(24), + onTap: () { + vm.toggleFavorite(); + }, + ); + }, + ), + const SizedBox( + width: 8, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: restaurant.heroImage, + imageBuilder: (context, imageProvider) => Container( + height: 440, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: Column( + children: [ + const SizedBox( + height: 24, + ), + Row( + children: [ + Text( + restaurant.price ?? kEmptyString, + style: context.theme.textTheme.bodyLarge, + ), + const SizedBox( + width: 4, + ), + Text( + restaurant.displayCategory, + style: context.theme.textTheme.bodyLarge, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Text( + restaurant.isOpen ? "Open Now" : "Closed", + style: context.theme.textTheme.bodyLarge, + ), + ), + ), + const SizedBox( + width: 8, + ), + Icon( + Icons.circle, + size: 14, + color: restaurant.isOpen + ? Colors.green.shade300 + : Colors.red, + ), + ], + ), + const SizedBox( + height: 24, + ), + Divider( + color: Colors.grey.shade200, + height: 1.5, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Text( + "Address", + style: context.theme.textTheme.bodyLarge, + ), + const SizedBox( + height: 24, + ), + Text( + restaurant.location?.formattedAddress ?? kEmptyString, + style: context.theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 24, + ), + Divider( + color: Colors.grey.shade200, + height: 1.5, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Text( + "Overall Rating", + style: context.theme.textTheme.bodyLarge, + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + Text( + restaurant.rating?.toString() ?? kEmptyString, + style: context.theme.textTheme.headlineLarge + ?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Padding( + padding: EdgeInsets.only( + top: 8, + ), + child: Icon( + Icons.star, + color: Colors.amber, + size: 18, + ), + ), + ], + ), + const SizedBox( + height: 24, + ), + Divider( + color: Colors.grey.shade200, + height: 1.5, + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + if (vm.showReviews) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${vm.restaurant?.reviews?.length} Reviews", + style: context.theme.textTheme.bodyLarge, + ), + const SizedBox( + height: 24, + ), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: restaurant.reviews?.length ?? kZeroInt, + padding: const EdgeInsets.only( + top: 16, + right: 8, + left: 8, + bottom: 36, + ), + itemBuilder: (context, index) { + final review = vm.restaurant?.reviews![index]; + + return ReviewItem( + rating: review?.rating ?? kZeroInt, + userName: review?.user?.name ?? kEmptyString, + userImgUrl: + review?.user?.imageUrl ?? kEmptyString, + ); + }, + separatorBuilder: (context, index) => + const SizedBox( + height: 10, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/restaurant_details/restaurant_details_vm.dart b/lib/screens/restaurant_details/restaurant_details_vm.dart new file mode 100644 index 00000000..916744a5 --- /dev/null +++ b/lib/screens/restaurant_details/restaurant_details_vm.dart @@ -0,0 +1,51 @@ +import 'package:flutter/cupertino.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class RestaurantDetailsVM extends ChangeNotifier { + Restaurant? restaurant; + + bool get showReviews => restaurant?.reviews?.isNotEmpty ?? false; + + late final _prefs = SharedPreferences.getInstance(); + + var _yelpRepository = YelpRepository(); + + void init(Restaurant restaurant) async { + this.restaurant = restaurant; + } + + Future toggleFavorite() async { + final restaurantIds = await _yelpRepository.getFavoriteRestaurantsIds(); + final restaurantId = restaurant?.id; + + if (restaurantId != null) { + if (await isFavorite()) { + restaurantIds.remove(restaurantId); + } else { + restaurantIds.add(restaurantId); + } + await _yelpRepository.setFavoriteRestaurantsIds(restaurantIds); + notifyListeners(); + } + } + + Future isFavorite() async { + final prefs = await _prefs; + final restaurantsIds = prefs.getStringList(kFavoriteRestaurantsKey) ?? []; + final restaurantId = restaurant?.id; + + if (restaurantId != null) { + return restaurantsIds.contains(restaurantId); + } else { + return false; + } + } + + @visibleForTesting + void setYelpRepository(YelpRepository repository) { + _yelpRepository = repository; + } +} diff --git a/lib/screens/restaurant_details/review_item.dart b/lib/screens/restaurant_details/review_item.dart new file mode 100644 index 00000000..d2f3d7ab --- /dev/null +++ b/lib/screens/restaurant_details/review_item.dart @@ -0,0 +1,83 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:restaurantour/common/extensions.dart'; + +class ReviewItem extends StatelessWidget { + const ReviewItem({ + super.key, + required this.rating, + required this.userName, + required this.userImgUrl, + }); + + final int rating; + final String userName; + final String userImgUrl; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RatingBar.builder( + initialRating: rating.toDouble(), + direction: Axis.horizontal, + allowHalfRating: true, + unratedColor: Colors.amber.withAlpha(50), + itemCount: 5, + itemSize: 20, + ignoreGestures: true, + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Colors.amber, + ), + onRatingUpdate: (rating) {}, + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + if (userImgUrl.isNotEmpty) + CachedNetworkImage( + imageUrl: userImgUrl, + imageBuilder: (context, imageProvider) => Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + placeholder: (_, __) => const SizedBox( + height: 80, + width: 80, + ), + ), + if (userImgUrl.isNotEmpty) + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + userName, + style: context.theme.textTheme.bodyLarge, + ), + ), + ], + ), + const SizedBox( + height: 24, + ), + Divider( + color: Colors.grey.shade200, + height: 1.5, + ), + ], + ); + } +} diff --git a/lib/screens/tabs/restaurant_item.dart b/lib/screens/tabs/restaurant_item.dart new file mode 100644 index 00000000..5611ad4a --- /dev/null +++ b/lib/screens/tabs/restaurant_item.dart @@ -0,0 +1,140 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/common/extensions.dart'; +import 'package:restaurantour/models/restaurant.dart'; + +class RestaurantItem extends StatelessWidget { + const RestaurantItem({ + super.key, + required this.restaurant, + required this.onTap, + }); + + final Restaurant restaurant; + final Function() onTap; + + @override + Widget build(BuildContext context) { + return Card( + surfaceTintColor: Colors.white, + color: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + elevation: 4, + shadowColor: Colors.grey.shade50, + child: InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all( + Radius.circular(8.0), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + CachedNetworkImage( + imageUrl: restaurant.heroImage, + imageBuilder: (context, imageProvider) => Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: const BorderRadius.all( + Radius.circular(8.0), + ), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + placeholder: (_, __) => const SizedBox( + height: 80, + width: 80, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? kEmptyString, + style: context.theme.textTheme.bodySmall?.copyWith( + fontSize: 20, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Text(restaurant.price ?? kEmptyString), + const SizedBox( + width: 4, + ), + Text(restaurant.displayCategory), + ], + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + RatingBar.builder( + initialRating: restaurant.rating ?? kZeroDouble, + direction: Axis.horizontal, + allowHalfRating: true, + unratedColor: Colors.amber.withAlpha(50), + itemCount: 5, + itemSize: 20, + ignoreGestures: true, + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Colors.amber, + ), + onRatingUpdate: (rating) {}, + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Align( + child: Text( + restaurant.isOpen ? "Open Now" : "Closed", + ), + alignment: Alignment.centerRight, + ), + ), + const SizedBox( + width: 8, + ), + Align( + child: Icon( + Icons.circle, + size: 14, + color: restaurant.isOpen + ? Colors.green.shade300 + : Colors.red, + ), + ), + ], + ) + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/tabs/restaurants_screen.dart b/lib/screens/tabs/restaurants_screen.dart new file mode 100644 index 00000000..50c14715 --- /dev/null +++ b/lib/screens/tabs/restaurants_screen.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:restaurantour/common/extensions.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/screens/restaurant_details/restaurant_details_screen.dart'; +import 'package:restaurantour/screens/tabs/restaurant_item.dart'; +import 'package:restaurantour/screens/tabs/tabs_vm.dart'; + +class RestaurantsScreen extends StatelessWidget { + const RestaurantsScreen({ + super.key, + required this.restaurants, + required this.loadStatus, + }); + + final List restaurants; + final LoadStatus loadStatus; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, vm, _) { + switch (loadStatus) { + case LoadStatus.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case LoadStatus.error: + return Center( + child: Text( + "ERROR!", + style: context.theme.textTheme.titleMedium, + ), + ); + case LoadStatus.loaded: + return ListView.separated( + itemCount: restaurants.length, + padding: const EdgeInsets.only( + top: 16, + right: 8, + left: 8, + bottom: 36, + ), + itemBuilder: (context, index) { + final restaurant = restaurants[index]; + + return RestaurantItem( + restaurant: restaurant, + onTap: () { + _openRestaurantDetails(context, restaurant, vm); + }, + ); + }, + separatorBuilder: (context, index) => const SizedBox( + height: 10, + ), + ); + } + }, + ); + } + + void _openRestaurantDetails( + BuildContext context, Restaurant restaurant, TabsVM vm) async { + await RestaurantDetails.push(context, restaurant); + await vm.getFavoriteRestaurants(); + vm.forceNotifyListeners(); + } +} diff --git a/lib/screens/tabs/tabs_screen.dart b/lib/screens/tabs/tabs_screen.dart new file mode 100644 index 00000000..af342e49 --- /dev/null +++ b/lib/screens/tabs/tabs_screen.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:restaurantour/custom_widget/custom_app_bar.dart'; +import 'package:restaurantour/screens/tabs/restaurants_screen.dart'; +import 'package:restaurantour/screens/tabs/tabs_vm.dart'; + +class TabsScreen extends StatefulWidget { + const TabsScreen({super.key}); + + @override + State createState() => _TabsScreenState(); +} + +class _TabsScreenState extends State { + late final _vm = TabsVM(); + + @override + void initState() { + _vm.getRestaurants(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _vm, + child: DefaultTabController( + length: 2, + child: Scaffold( + appBar: getCustomAppBar( + context, + "RestauranTour", + titleAlignment: Alignment.center, + bottom: TabBar( + overlayColor: MaterialStateProperty.all(Colors.transparent), + tabs: const [ + Tab( + child: Text( + "All Restautants", + style: TextStyle( + fontSize: 18, + color: Colors.black, + ), + ), + ), + Tab( + child: Text( + "My Favorites", + style: TextStyle( + fontSize: 18, + color: Colors.black, + ), + ), + ), + ], + ), + ), + body: Container( + color: Colors.grey.shade100, + child: Consumer( + builder: (context, vm, _) { + return TabBarView( + children: [ + RestaurantsScreen( + loadStatus: vm.loadStatus, + restaurants: vm.restaurants, + ), + RestaurantsScreen( + loadStatus: vm.loadStatus, + restaurants: vm.favoriteRestaurants, + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/tabs/tabs_vm.dart b/lib/screens/tabs/tabs_vm.dart new file mode 100644 index 00000000..cb6b3789 --- /dev/null +++ b/lib/screens/tabs/tabs_vm.dart @@ -0,0 +1,57 @@ +import 'package:flutter/cupertino.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +enum LoadStatus { loaded, loading, error } + +class TabsVM extends ChangeNotifier { + var _yelpRepository = YelpRepository(); + + var loadStatus = LoadStatus.loading; + + List restaurants = []; + List favoriteRestaurants = []; + + Future getRestaurants() async { + try { + restaurants.clear(); + + final response = await _yelpRepository.getRestaurants(); + loadStatus = LoadStatus.loaded; + + restaurants = response?.restaurants ?? []; + await getFavoriteRestaurants(); + + notifyListeners(); + } catch (e) { + debugPrint(e.toString()); + loadStatus = LoadStatus.error; + notifyListeners(); + } + } + + Future getFavoriteRestaurants() async { + favoriteRestaurants.clear(); + + try { + final restaurantIds = await _yelpRepository.getFavoriteRestaurantsIds(); + + for (var restaurant in restaurants) { + if (restaurantIds.contains(restaurant.id)) { + favoriteRestaurants.add(restaurant); + } + } + } catch (e) { + debugPrint(e.toString()); + } + } + + void forceNotifyListeners() { + notifyListeners(); + } + + @visibleForTesting + void setYelpRepository(YelpRepository repository) { + _yelpRepository = repository; + } +} diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..9873ffa9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -61,34 +61,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.0" built_collection: dependency: transitive description: @@ -101,34 +101,50 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" - characters: + version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: dependency: transitive description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: + version: "4.0.0" + cached_network_image_web: dependency: transitive description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.1.1" + 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: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -157,42 +173,42 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.3+1" fake_async: dependency: transitive description: @@ -201,27 +217,43 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_lints: dependency: "direct dev" description: @@ -230,35 +262,48 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: @@ -267,38 +312,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -327,10 +380,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -359,18 +412,42 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,54 +464,190 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -464,6 +677,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" stack_trace: dependency: transitive description: @@ -484,10 +721,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -496,6 +733,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -516,42 +761,50 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + url: "https://pub.dev" + source: hosted + version: "4.2.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -564,10 +817,10 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: @@ -580,26 +833,42 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.0-0" + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..0d8ef482 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: @@ -16,6 +16,10 @@ dependencies: dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + provider: ^6.1.2 + cached_network_image: ^3.3.1 + flutter_rating_bar: ^4.0.1 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: @@ -23,8 +27,9 @@ dev_dependencies: flutter_lints: ^1.0.2 build_runner: ^2.4.8 json_serializable: ^6.7.1 + mockito: ^5.4.4 flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + assets: + - assets/mock_restaurants.json \ No newline at end of file diff --git a/test/viewmodel/restaurant_details_vm/restaurant_details_vm_test.dart b/test/viewmodel/restaurant_details_vm/restaurant_details_vm_test.dart new file mode 100644 index 00000000..8f701aab --- /dev/null +++ b/test/viewmodel/restaurant_details_vm/restaurant_details_vm_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/screens/restaurant_details/restaurant_details_vm.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../tabs_vm/tabs_vm_test.mocks.dart'; + +@GenerateMocks([YelpRepository]) +void main() { + final _mockYelpRepository = MockYelpRepository(); + + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + }); + + test( + 'test toggleFavorite add favorite', + () async { + final vm = RestaurantDetailsVM(); + + SharedPreferences.setMockInitialValues( + { + kFavoriteRestaurantsKey: [], + }, + ); + + vm.setYelpRepository(_mockYelpRepository); + + when( + _mockYelpRepository.getFavoriteRestaurantsIds(), + ).thenAnswer( + (_) => Future.value( + [], + ), + ); + + final restaurant = Restaurant.fromJson(fakeRestaurant); + + vm.init(restaurant); + + await vm.toggleFavorite(); + + verify(_mockYelpRepository.getFavoriteRestaurantsIds()); + verify( + _mockYelpRepository.setFavoriteRestaurantsIds( + [restaurant.id ?? kEmptyString], + ), + ); + }, + ); + + test( + 'test toggleFavorite remove favorite', + () async { + final vm = RestaurantDetailsVM(); + + SharedPreferences.setMockInitialValues( + { + kFavoriteRestaurantsKey: ["vHz2RLtfUMVRPFmd7VBEHA"], + }, + ); + + vm.setYelpRepository(_mockYelpRepository); + + when( + _mockYelpRepository.getFavoriteRestaurantsIds(), + ).thenAnswer( + (_) => Future.value( + ["vHz2RLtfUMVRPFmd7VBEHA"], + ), + ); + + final restaurant = Restaurant.fromJson(fakeRestaurant); + + vm.init(restaurant); + + await vm.toggleFavorite(); + + verify(_mockYelpRepository.getFavoriteRestaurantsIds()); + verify( + _mockYelpRepository.setFavoriteRestaurantsIds( + [], + ), + ); + }, + ); +} + +Map get fakeRestaurant => { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": '\$\$\$', + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg", + ], + "reviews": [ + { + "id": "VzJIMZRW-8lwoFJzk0jAXw", + "rating": 5, + "user": { + "id": "i2dS47auJ-9-OW4xZSPxAA", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/M2AsmeEgwVwpjyaE1lFtIA/o.jpg", + "name": "White R.", + }, + }, + { + "id": "H85bnGMvTx0ACssHvyCyug", + "rating": 5, + "user": { + "id": "3xfzp3cOhKICnLn0D9ZheA", + "image_url": null, + "name": "Molly S.", + }, + }, + { + "id": "O60EsMnhEmrSs4F54imSkw", + "rating": 3, + "user": { + "id": "C8e7rVhQY6lMYm-yn1luJQ", + "image_url": null, + "name": "Taylor T.", + }, + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Seafood", "alias": "seafood"}, + ], + "hours": [ + {"is_open_now": true}, + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109", + }, + }; diff --git a/test/viewmodel/tabs_vm/tabs_vm_test.dart b/test/viewmodel/tabs_vm/tabs_vm_test.dart new file mode 100644 index 00000000..35c70c55 --- /dev/null +++ b/test/viewmodel/tabs_vm/tabs_vm_test.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurantour/common/constants.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/screens/tabs/tabs_vm.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'tabs_vm_test.mocks.dart'; + +@GenerateMocks([YelpRepository]) +void main() { + final mockYelpRepository = MockYelpRepository(); + final tabsVM = TabsVM(); + + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + }); + + test( + 'test getRestaurants Success and getFavoriteRestaurantsIds empty', + () async { + SharedPreferences.setMockInitialValues( + { + kFavoriteRestaurantsKey: [], + }, + ); + + tabsVM.setYelpRepository(mockYelpRepository); + + when( + mockYelpRepository.getRestaurants(), + ).thenAnswer( + (_) => Future.value( + RestaurantQueryResult.fromJson(fakeRestaurants), + ), + ); + + when( + mockYelpRepository.getFavoriteRestaurantsIds(), + ).thenAnswer( + (_) => Future.value( + [], + ), + ); + + await tabsVM.getRestaurants(); + + assert(tabsVM.restaurants.length == 2); + assert(tabsVM.favoriteRestaurants.isEmpty); + }, + ); + + test( + 'test getRestaurants Success and getFavoriteRestaurantsIds not empty', + () async { + SharedPreferences.setMockInitialValues( + { + kFavoriteRestaurantsKey: [], + }, + ); + + tabsVM.setYelpRepository(mockYelpRepository); + + when( + mockYelpRepository.getRestaurants(), + ).thenAnswer( + (_) => Future.value( + RestaurantQueryResult.fromJson(fakeRestaurants), + ), + ); + + when( + mockYelpRepository.getFavoriteRestaurantsIds(), + ).thenAnswer( + (_) => Future.value( + ["vHz2RLtfUMVRPFmd7VBEHA"], + ), + ); + + await tabsVM.getRestaurants(); + print(tabsVM.restaurants.length); + + assert(tabsVM.restaurants.length == 2); + assert(tabsVM.favoriteRestaurants.isNotEmpty); + }, + ); + + test( + 'test getRestaurants Error', + () async { + SharedPreferences.setMockInitialValues( + { + kFavoriteRestaurantsKey: [], + }, + ); + + tabsVM.setYelpRepository(mockYelpRepository); + + when( + mockYelpRepository.getRestaurants(), + ).thenThrow( + Exception(), + ); + + when( + mockYelpRepository.getFavoriteRestaurantsIds(), + ).thenAnswer( + (_) => Future.value( + [], + ), + ); + + await tabsVM.getRestaurants(); + print(tabsVM.restaurants.length); + + assert(tabsVM.restaurants.isEmpty); + assert(tabsVM.restaurants.isEmpty); + }, + ); +} + +Map get fakeRestaurants => { + "total": 6251, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": '\$\$\$', + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg", + ], + "reviews": [ + { + "id": "VzJIMZRW-8lwoFJzk0jAXw", + "rating": 5, + "user": { + "id": "i2dS47auJ-9-OW4xZSPxAA", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/M2AsmeEgwVwpjyaE1lFtIA/o.jpg", + "name": "White R.", + }, + }, + { + "id": "H85bnGMvTx0ACssHvyCyug", + "rating": 5, + "user": { + "id": "3xfzp3cOhKICnLn0D9ZheA", + "image_url": null, + "name": "Molly S.", + }, + }, + { + "id": "O60EsMnhEmrSs4F54imSkw", + "rating": 3, + "user": { + "id": "C8e7rVhQY6lMYm-yn1luJQ", + "image_url": null, + "name": "Taylor T.", + }, + } + ], + "categories": [ + {"title": "New American", "alias": "newamerican"}, + {"title": "Seafood", "alias": "seafood"}, + ], + "hours": [ + {"is_open_now": true}, + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109", + }, + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "\$\$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg", + ], + "reviews": [ + { + "id": "uIeZrx9X1W0XPKqDicXZew", + "rating": 5, + "user": { + "id": "nvcvPpKYpq-nT7wwAexGYw", + "image_url": + "https://s3-media1.fl.yelpcdn.com/photo/2_pHFKGZ3-SlBq_HTXp8wg/o.jpg", + "name": "Tanner D.", + }, + }, + { + "id": "V8KFADRFJnsGUvQ3iRtnig", + "rating": 5, + "user": { + "id": "R_PPnsl0gsIvzhq9JHRCXQ", + "image_url": null, + "name": "Misha Z.", + }, + }, + { + "id": "NTi315CS824pvOsnqZmnww", + "rating": 5, + "user": { + "id": "swpNJGPBG4XCiMH7FeOUZg", + "image_url": + "https://s3-media2.fl.yelpcdn.com/photo/Tft5UPZtZ4zMNtVgdvbIwQ/o.jpg", + "name": "Kimberly Z.", + }, + } + ], + "categories": [ + {"title": "Southern", "alias": "southern"}, + {"title": "New American", "alias": "newamerican"}, + {"title": "Cocktail Bars", "alias": "cocktailbars"}, + ], + "hours": [ + {"is_open_now": true}, + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109", + }, + }, + ], + }; diff --git a/test/viewmodel/tabs_vm/tabs_vm_test.mocks.dart b/test/viewmodel/tabs_vm/tabs_vm_test.mocks.dart new file mode 100644 index 00000000..224e43a7 --- /dev/null +++ b/test/viewmodel/tabs_vm/tabs_vm_test.mocks.dart @@ -0,0 +1,871 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurantour/test/viewmodel/tabs_vm/tabs_vm_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:dio/dio.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:restaurantour/models/restaurant.dart' as _i5; +import 'package:restaurantour/repositories/yelp_repository.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDio_0 extends _i1.SmartFake implements _i2.Dio { + _FakeDio_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBaseOptions_1 extends _i1.SmartFake implements _i2.BaseOptions { + _FakeBaseOptions_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientAdapter_2 extends _i1.SmartFake + implements _i2.HttpClientAdapter { + _FakeHttpClientAdapter_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTransformer_3 extends _i1.SmartFake implements _i2.Transformer { + _FakeTransformer_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInterceptors_4 extends _i1.SmartFake implements _i2.Interceptors { + _FakeInterceptors_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResponse_5 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [YelpRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockYelpRepository extends _i1.Mock implements _i3.YelpRepository { + MockYelpRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Dio get dio => (super.noSuchMethod( + Invocation.getter(#dio), + returnValue: _FakeDio_0( + this, + Invocation.getter(#dio), + ), + ) as _i2.Dio); + + @override + set dio(_i2.Dio? _dio) => super.noSuchMethod( + Invocation.setter( + #dio, + _dio, + ), + returnValueForMissingStub: null, + ); + + @override + void setDio(_i2.Dio? dio) => super.noSuchMethod( + Invocation.method( + #setDio, + [dio], + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i5.RestaurantQueryResult?> getRestaurants({int? offset = 0}) => + (super.noSuchMethod( + Invocation.method( + #getRestaurants, + [], + {#offset: offset}, + ), + returnValue: _i4.Future<_i5.RestaurantQueryResult?>.value(), + ) as _i4.Future<_i5.RestaurantQueryResult?>); + + @override + _i4.Future<_i5.RestaurantQueryResult?> getRestaurantsMocked( + {int? offset = 0}) => + (super.noSuchMethod( + Invocation.method( + #getRestaurantsMocked, + [], + {#offset: offset}, + ), + returnValue: _i4.Future<_i5.RestaurantQueryResult?>.value(), + ) as _i4.Future<_i5.RestaurantQueryResult?>); + + @override + _i4.Future> getFavoriteRestaurantsIds() => (super.noSuchMethod( + Invocation.method( + #getFavoriteRestaurantsIds, + [], + ), + returnValue: _i4.Future>.value([]), + ) as _i4.Future>); + + @override + _i4.Future setFavoriteRestaurantsIds(List? restaurantIds) => + (super.noSuchMethod( + Invocation.method( + #setFavoriteRestaurantsIds, + [restaurantIds], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} + +/// A class which mocks [Dio]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDio extends _i1.Mock implements _i2.Dio { + MockDio() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.BaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeBaseOptions_1( + this, + Invocation.getter(#options), + ), + ) as _i2.BaseOptions); + + @override + set options(_i2.BaseOptions? _options) => super.noSuchMethod( + Invocation.setter( + #options, + _options, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( + Invocation.getter(#httpClientAdapter), + returnValue: _FakeHttpClientAdapter_2( + this, + Invocation.getter(#httpClientAdapter), + ), + ) as _i2.HttpClientAdapter); + + @override + set httpClientAdapter(_i2.HttpClientAdapter? _httpClientAdapter) => + super.noSuchMethod( + Invocation.setter( + #httpClientAdapter, + _httpClientAdapter, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.Transformer get transformer => (super.noSuchMethod( + Invocation.getter(#transformer), + returnValue: _FakeTransformer_3( + this, + Invocation.getter(#transformer), + ), + ) as _i2.Transformer); + + @override + set transformer(_i2.Transformer? _transformer) => super.noSuchMethod( + Invocation.setter( + #transformer, + _transformer, + ), + returnValueForMissingStub: null, + ); + + @override + _i2.Interceptors get interceptors => (super.noSuchMethod( + Invocation.getter(#interceptors), + returnValue: _FakeInterceptors_4( + this, + Invocation.getter(#interceptors), + ), + ) as _i2.Interceptors); + + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method( + #close, + [], + {#force: force}, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i2.Response> head( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> headUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> get( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #get, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> getUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #getUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> post( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> postUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> put( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> putUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> patch( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> patchUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> delete( + String? path, { + Object? data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> deleteUri( + Uri? uri, { + Object? data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> download( + String? urlPath, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + Map? queryParameters, + _i2.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> downloadUri( + Uri? uri, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + _i2.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + Object? data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> request( + String? url, { + Object? data, + Map? queryParameters, + _i2.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #request, + [url], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> requestUri( + Uri? uri, { + Object? data, + _i2.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> fetch(_i2.RequestOptions? requestOptions) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [requestOptions], + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_5( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + ) as _i4.Future<_i2.Response>); +} diff --git a/test/widget/restaurant_details_screen/restaurant_details_screen_test.dart b/test/widget/restaurant_details_screen/restaurant_details_screen_test.dart new file mode 100644 index 00000000..f8724e7a --- /dev/null +++ b/test/widget/restaurant_details_screen/restaurant_details_screen_test.dart @@ -0,0 +1,57 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/screens/tabs/restaurants_screen.dart'; +import 'package:restaurantour/screens/tabs/tabs_vm.dart'; + +import '../../viewmodel/tabs_vm/tabs_vm_test.dart'; + +void main() { + testWidgets( + 'test RestaurantDetailsScreen', + (WidgetTester tester) async { + final vm = TabsVM(); + + final restaurants = + RestaurantQueryResult + .fromJson(fakeRestaurants) + .restaurants; + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: vm, + child: RestaurantsScreen( + restaurants: restaurants!, + loadStatus: LoadStatus.loaded, + ), + ), + ), + ); + + await tester.pump( + Durations.short3, + ); + + final node = find.byType(InkWell); + + await tester.tap(node.first); + + await tester.pumpAndSettle(); + + expect(find.text('\$\$\$'), findsOneWidget); + expect(find.text('New American'), findsOneWidget); + expect(find.text('Address'), findsOneWidget); + expect(find.text('4.4'), findsOneWidget); + expect(find.byType(RatingBar), findsNWidgets(3)); + expect(find.byType(CachedNetworkImage), findsNWidgets(2)); + expect(find.text('White R.'), findsOneWidget); + expect(find.text('Molly S.'), findsOneWidget); + expect(find.text('Taylor T.'), findsOneWidget); + expect(find.text('Open Now'), findsOneWidget); + }, + ); +} \ No newline at end of file diff --git a/test/widget/restaurant_screen/restaurant_screen_test.dart b/test/widget/restaurant_screen/restaurant_screen_test.dart new file mode 100644 index 00000000..8dc752e4 --- /dev/null +++ b/test/widget/restaurant_screen/restaurant_screen_test.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/screens/tabs/restaurants_screen.dart'; +import 'package:restaurantour/screens/tabs/tabs_vm.dart'; + +import '../../viewmodel/tabs_vm/tabs_vm_test.dart'; + +void main() { + testWidgets( + 'test RestaurantScreen loaded', + (WidgetTester tester) async { + final restaurants = + RestaurantQueryResult.fromJson(fakeRestaurants).restaurants; + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: TabsVM(), + child: RestaurantsScreen( + restaurants: restaurants!, + loadStatus: LoadStatus.loaded, + ), + ), + ), + ); + + await tester.pump( + Durations.short3, + ); + + expect(find.text('Gordon Ramsay Hell\'s Kitchen'), findsOneWidget); + expect(find.text('Yardbird'), findsOneWidget); + }, + ); + + testWidgets( + 'test RestaurantScreen loading', + (WidgetTester tester) async { + final vm = TabsVM(); + final restaurants = + RestaurantQueryResult.fromJson(fakeRestaurants).restaurants; + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: vm, + child: RestaurantsScreen( + restaurants: restaurants!, + loadStatus: LoadStatus.loading, + ), + ), + ), + ); + + await tester.pump( + Durations.short3, + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgets( + 'test RestaurantScreen error', + (WidgetTester tester) async { + final vm = TabsVM(); + + final restaurants = + RestaurantQueryResult.fromJson(fakeRestaurants).restaurants; + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: vm, + child: RestaurantsScreen( + restaurants: restaurants!, + loadStatus: LoadStatus.error, + ), + ), + ), + ); + + await tester.pump( + Durations.short3, + ); + + expect(find.bySemanticsLabel("ERROR!"), findsOneWidget); + }, + ); +}