diff --git a/build/testfile.dill b/build/testfile.dill deleted file mode 100644 index 713c092..0000000 Binary files a/build/testfile.dill and /dev/null differ diff --git a/build/testfile.dill.track.dill b/build/testfile.dill.track.dill deleted file mode 100644 index 6105ec0..0000000 Binary files a/build/testfile.dill.track.dill and /dev/null differ diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 3b19a8a..068d682 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -6,7 +6,8 @@ analyzer: unused_local_variable: error dead_code: error exclude: - - example/lib/json/weather_in_cities.g.dart + - lib/service/json/weather_in_cities.g.dart + linter: rules: diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 2603af2..6363669 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -22,7 +22,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.flutterweatherdemo" minSdkVersion 16 targetSdkVersion 27 @@ -33,8 +32,6 @@ android { buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index d41505d..44ffa37 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -2,14 +2,13 @@ # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/Users/oleksandr.babich/development/flutter" export "FLUTTER_APPLICATION_PATH=/Users/oleksandr.babich/development/rx/rx_widgets/example" -export "FLUTTER_TARGET=/Users/oleksandr.babich/development/rx/rx_widgets/example/lib/main.dart" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "OTHER_LDFLAGS=$(inherited) -framework Flutter" -export "FLUTTER_FRAMEWORK_DIR=/Users/oleksandr.babich/development/flutter/bin/cache/artifacts/engine/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" +export "TRACK_WIDGET_CREATION=false" export "TREE_SHAKE_ICONS=false" export "PACKAGE_CONFIG=.packages" diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 59c6d39..919434a 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/example/lib/homepage/home_page.dart b/example/lib/homepage/home_page.dart new file mode 100644 index 0000000..73df03a --- /dev/null +++ b/example/lib/homepage/home_page.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:rx_widget_demo/homepage/weather_list_view.dart'; +import 'package:rx_widget_demo/keys.dart'; +import 'package:rx_widget_demo/model_provider.dart'; +import 'package:rx_widget_demo/service/weather_entry.dart'; +import 'package:rx_widgets/rx_widgets.dart'; + +// ignore_for_file: deprecated_member_use + +const noResultsText = 'No matching city data found. Try refining search.'; + +class HomePage extends StatelessWidget { + final TextEditingController _controller = TextEditingController(); + + HomePage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final homePageModel = ModelProvider.of(context); + /*return Scaffold( + appBar: AppBar(title: AppBar(title: Text('WeatherDemo'))), + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + key: AppKeys.textField, + autocorrect: false, + controller: _controller, + decoration: InputDecoration( + hintText: "Filter cities", + ), + style: TextStyle( + fontSize: 20.0, + ), + onChanged: (s) => homePageModel.textChangedCommand(s), + // onChanged: ModelProvider.of(context).textChangedCommand, + ), + ), + Text('My Message'), + ], + ), + ), + );*/ + return Scaffold( + appBar: AppBar(title: Text("WeatherDemo")), + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + key: AppKeys.textField, + autocorrect: false, + controller: _controller, + decoration: InputDecoration(hintText: "Filter cities"), + style: TextStyle( + fontSize: 20.0, + ), + onChanged: (s) => homePageModel.textChangedCommand(s), + ), + ), + Expanded( + child: RxLoader>( + spinnerKey: AppKeys.loadingSpinner, + radius: 25.0, + commandResults: homePageModel.updateWeatherCommand.results, + dataBuilder: (_, data) { + if (data.isEmpty) return Center(child: Text(noResultsText)); + return WeatherListView(data, key: AppKeys.weatherList); + }, + placeHolderBuilder: (_) => Center( + key: AppKeys.loaderPlaceHolder, child: Text("No Data")), + errorBuilder: (_, ex) => Center( + key: AppKeys.loaderError, + child: Text("Error: ${ex.toString()}")), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + // This might be solved with a StreamBuilder to but it should show `WidgetSelector` + child: WidgetSelector( + key: AppKeys.widgetSelector, + buildEvents: + homePageModel.updateWeatherCommand.canExecute, + //We access our ViewModel through the inherited Widget + onTrue: RaisedButton( + key: AppKeys.updateButtonEnabled, + child: Text("Update"), + onPressed: () { + _controller.clear(); + homePageModel.updateWeatherCommand(''); + }, + ), + onFalse: RaisedButton( + key: AppKeys.updateButtonDisabled, + child: Text("Please Wait"), + onPressed: null, + ), + ), + ), + StateFullSwitch( + state: true, + onChanged: (b) => homePageModel.switchChangedCommand(b), + // onChanged: homePageModel.switchChangedCommand, + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +/// As the normal switch does not even remember and display its current state +/// we us this one +class StateFullSwitch extends StatefulWidget { + final bool state; + final ValueChanged onChanged; + + StateFullSwitch({required this.state, required this.onChanged}); + + @override + StateFullSwitchState createState() { + return StateFullSwitchState(state, onChanged); + } +} + +class StateFullSwitchState extends State { + bool state; + ValueChanged handler; + + StateFullSwitchState(this.state, this.handler); + + @override + Widget build(BuildContext context) { + return Switch( + key: AppKeys.updateSwitch, + value: state, + onChanged: (b) { + setState(() => state = b); + handler(b); + }, + ); + } +} diff --git a/example/lib/homepage/homepage.dart b/example/lib/homepage/homepage.dart deleted file mode 100644 index 5fe99ab..0000000 --- a/example/lib/homepage/homepage.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rx_widget_demo/homepage/weather_list_view.dart'; -import 'package:rx_widget_demo/keys.dart'; -import 'package:rx_widget_demo/model_provider.dart'; -import 'package:rx_widgets/rx_widgets.dart'; - -import 'package:rx_widget_demo/service/weather_entry.dart'; - -class HomePage extends StatefulWidget { - @override - HomePageState createState() { - return HomePageState(); - } -} - -class HomePageState extends State { - final TextEditingController _controller = TextEditingController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("WeatherDemo")), - resizeToAvoidBottomPadding: false, - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - key: AppKeys.textField, - autocorrect: false, - controller: _controller, - decoration: InputDecoration( - hintText: "Filter cities", - ), - style: TextStyle( - fontSize: 20.0, - ), - onChanged: ModelProvider.of(context).textChangedCommand, - ), - ), - Expanded( - child: RxLoader>( - spinnerKey: AppKeys.loadingSpinner, - radius: 25.0, - commandResults: ModelProvider.of(context).updateWeatherCommand.results, - dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), - placeHolderBuilder: (context) => Center(key: AppKeys.loaderPlaceHolder, child: Text("No Data")), - errorBuilder: (context, ex) => Center(key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - // This might be solved with a StreamBuilder to but it should show `WidgetSelector` - child: WidgetSelector( - buildEvents: ModelProvider.of(context) - .updateWeatherCommand - .canExecute, //We access our ViewModel through the inherited Widget - onTrue: RaisedButton( - key: AppKeys.updateButtonEnabled, - child: Text("Update"), - onPressed: () { - _controller.clear(); - ModelProvider.of(context).updateWeatherCommand(); - }, - ), - onFalse: RaisedButton( - key: AppKeys.updateButtonDisabled, - child: Text("Please Wait"), - onPressed: null, - ), - ), - ), - StateFullSwitch( - state: true, - onChanged: ModelProvider.of(context).switchChangedCommand, - ) - ], - ), - ), - ], - ), - ); - } -} - -/// As the normal switch does not even remember and display its current state -/// we us this one -class StateFullSwitch extends StatefulWidget { - final bool state; - final ValueChanged onChanged; - - StateFullSwitch({this.state, this.onChanged}); - - @override - StateFullSwitchState createState() { - return StateFullSwitchState(state, onChanged); - } -} - -class StateFullSwitchState extends State { - bool state; - ValueChanged handler; - - StateFullSwitchState(this.state, this.handler); - - @override - Widget build(BuildContext context) { - return Switch( - key: AppKeys.updateSwitch, - value: state, - onChanged: (b) { - setState(() => state = b); - handler(b); - }, - ); - } -} diff --git a/example/lib/homepage/homepage_model.dart b/example/lib/homepage/homepage_model.dart index d19ce33..6df5177 100644 --- a/example/lib/homepage/homepage_model.dart +++ b/example/lib/homepage/homepage_model.dart @@ -1,8 +1,7 @@ - import 'package:rx_command/rx_command.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:rx_widget_demo/service/weather_service.dart'; import 'package:rx_widget_demo/service/weather_entry.dart'; +import 'package:rx_widget_demo/service/weather_service.dart'; +import 'package:rxdart/rxdart.dart'; class HomePageModel { final WeatherService service; @@ -17,7 +16,7 @@ class HomePageModel { this.service, ); - factory HomePageModel(WeatherService service ) { + factory HomePageModel(WeatherService service) { // Command expects a bool value when executed and issues the value on it's // result Observable (stream) final _switchChangedCommand = RxCommand.createSync((b) => b); @@ -26,7 +25,9 @@ class HomePageModel { // the updateWeatherCommand final _updateWeatherCommand = RxCommand.createAsync>( - service.getWeatherEntriesForCity, canExecute: _switchChangedCommand); + (city) => service.getWeatherEntriesForCity(city), + restriction: _switchChangedCommand, + ); // Will be called on every change of the search field final _textChangedCommand = RxCommand.createSync((s) => s); @@ -34,14 +35,14 @@ class HomePageModel { // When the user starts typing _textChangedCommand // Wait for the user to stop typing for 500ms - .debounceTime( Duration(milliseconds: 500)) + .debounceTime(Duration(milliseconds: 500)) // Then call the updateWeatherCommand .listen(_updateWeatherCommand); // Update data on startup _updateWeatherCommand(''); - return HomePageModel._( + return HomePageModel._( _updateWeatherCommand, _switchChangedCommand, _textChangedCommand, diff --git a/example/lib/homepage/weather_list_view.dart b/example/lib/homepage/weather_list_view.dart index ee51f16..c9e2790 100644 --- a/example/lib/homepage/weather_list_view.dart +++ b/example/lib/homepage/weather_list_view.dart @@ -4,39 +4,43 @@ import 'package:rx_widget_demo/keys.dart'; import 'package:rx_widget_demo/service/weather_entry.dart'; import 'package:rx_widget_demo/weather_icons.dart'; - class WeatherListView extends StatelessWidget { - final List data; - - WeatherListView(this.data, {Key key}) : super(key: key); + WeatherListView(this.data, {Key? key}) : super(key: key); - @override Widget build(BuildContext context) { return ListView.builder( - key: AppKeys.cityList, - itemCount: data.length, - itemBuilder: (BuildContext context, int index) => - WeatherItem(entry: data[index]), - ); - } + key: AppKeys.cityList, + itemCount: data.length, + itemBuilder: (BuildContext context, int index) => + WeatherItem(entry: data[index]), + ); + } } class WeatherItem extends StatelessWidget { final WeatherEntry entry; - WeatherItem({Key key, @required this.entry}) : super(key: key); + WeatherItem({Key? key, required this.entry}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( - leading: Icon(_weatherIdToIcon(entry.weatherId), size: 28.0,), - title: Text(entry.cityName), - subtitle: Text(entry.description, style: TextStyle(fontStyle: FontStyle.italic),), - trailing: Text('${entry.temperature.round()} °', style: TextStyle(fontSize: 20.0), - ), + leading: Icon( + _weatherIdToIcon(entry.weatherId), + size: 28.0, + ), + title: Text(entry.cityName), + subtitle: Text( + entry.description, + style: TextStyle(fontStyle: FontStyle.italic), + ), + trailing: Text( + '${entry.temperature.round()} °', + style: TextStyle(fontSize: 20.0), + ), ); } diff --git a/example/lib/keys.dart b/example/lib/keys.dart index c4b5247..1df9f4c 100644 --- a/example/lib/keys.dart +++ b/example/lib/keys.dart @@ -9,6 +9,7 @@ class AppKeys { static final Key loadingSpinner = new Key('loadingSpinner'); static final Key updateButtonEnabled = new Key('updateButtonEnabled'); static final Key updateButtonDisabled = new Key('updateButtonDisabled'); + static final Key widgetSelector = new Key('widgetSelector'); static final Key loaderPlaceHolder = new Key('loaderPlaceHolder'); static final Key loaderError = new Key('loaderError'); static final Key updateSwitch = new Key('updateSwitch'); diff --git a/example/lib/main.dart b/example/lib/main.dart index 504fae7..d245f7d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,24 +1,23 @@ import 'package:flutter/material.dart'; - -import 'package:rx_widget_demo/homepage/homepage.dart'; +import 'package:http/http.dart' as http; +import 'package:rx_widget_demo/homepage/home_page.dart'; import 'package:rx_widget_demo/homepage/homepage_model.dart'; import 'package:rx_widget_demo/model_provider.dart'; import 'package:rx_widget_demo/service/weather_service.dart'; -import 'package:http/http.dart' as http; void main() { final weatherService = WeatherService(http.Client()); final homePageModel = HomePageModel(weatherService); - runApp(MyApp( - model: homePageModel, - )); + runApp( + MyApp(model: homePageModel), + ); } class MyApp extends StatelessWidget { final HomePageModel model; - const MyApp({Key key, this.model}) : super(key: key); + const MyApp({Key? key, required this.model}) : super(key: key); @override Widget build(BuildContext context) { @@ -37,4 +36,4 @@ class MyApp extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/example/lib/model_provider.dart b/example/lib/model_provider.dart index b05d2c1..be02488 100644 --- a/example/lib/model_provider.dart +++ b/example/lib/model_provider.dart @@ -2,20 +2,20 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:rx_widget_demo/homepage/homepage_model.dart'; - // InheritedWidgets allow you to propagate values down the Widget Tree. // it can then be accessed by just writing TheViewModel.of(context) class ModelProvider extends InheritedWidget { final HomePageModel model; - const ModelProvider({Key key, @required this.model, @required Widget child}) - : assert(model != null), - assert(child != null), - super(key: key, child: child); + const ModelProvider({Key? key, required this.model, required Widget child}) + : super(key: key, child: child); - static HomePageModel of(BuildContext context) => - (context.dependOnInheritedWidgetOfExactType()) - .model; + static HomePageModel of(BuildContext context) { + final type = context.dependOnInheritedWidgetOfExactType(); + final model = type?.model; + assert(model != null); + return model!; + } @override bool updateShouldNotify(ModelProvider oldWidget) => model != oldWidget.model; diff --git a/example/lib/service/json/weather_in_cities.dart b/example/lib/service/json/weather_in_cities.dart index f139e16..86d46cf 100644 --- a/example/lib/service/json/weather_in_cities.dart +++ b/example/lib/service/json/weather_in_cities.dart @@ -5,7 +5,7 @@ import "package:json_annotation/json_annotation.dart"; part "weather_in_cities.g.dart"; @JsonSerializable() -class WeatherInCities extends Object with _$WeatherInCitiesSerializerMixin { +class WeatherInCities { WeatherInCities(this.cnt, this.calctime, this.cod, this.cities); @JsonKey(name: 'cnt') @@ -25,7 +25,7 @@ class WeatherInCities extends Object with _$WeatherInCitiesSerializerMixin { } @JsonSerializable() -class City extends Object with _$CitySerializerMixin { +class City { City(this.id, this.coord, this.clouds, this.dt, this.name, this.main, this.rain, this.weather, this.wind); @@ -48,7 +48,7 @@ class City extends Object with _$CitySerializerMixin { final Main main; @JsonKey(name: 'rain') - final Rain rain; + final Rain? rain; @JsonKey(name: 'weather') final List weather; @@ -60,7 +60,7 @@ class City extends Object with _$CitySerializerMixin { } @JsonSerializable() -class Coord extends Object with _$CoordSerializerMixin { +class Coord { Coord(this.lat, this.lon); @JsonKey(name: 'Lat') @@ -73,7 +73,7 @@ class Coord extends Object with _$CoordSerializerMixin { } @JsonSerializable() -class Clouds extends Object with _$CloudsSerializerMixin { +class Clouds { Clouds(this.today); @JsonKey(name: 'today') @@ -83,18 +83,18 @@ class Clouds extends Object with _$CloudsSerializerMixin { } @JsonSerializable() -class Main extends Object with _$MainSerializerMixin { +class Main { Main(this.seaLevel, this.humidity, this.grndLevel, this.pressure, this.tempMax, this.temp, this.tempMin); - @JsonKey(name: 'sea_level', nullable: true) - final double seaLevel; + @JsonKey(name: 'sea_level') + final double? seaLevel; @JsonKey(name: 'humidity') final int humidity; - @JsonKey(name: 'grnd_level', nullable: true) - final double grndLevel; + @JsonKey(name: 'grnd_level') + final double? grndLevel; @JsonKey(name: 'pressure') final double pressure; @@ -112,7 +112,7 @@ class Main extends Object with _$MainSerializerMixin { } @JsonSerializable() -class Rain extends Object with _$RainSerializerMixin { +class Rain { Rain(this.threeH); @JsonKey(name: '3h') @@ -122,7 +122,7 @@ class Rain extends Object with _$RainSerializerMixin { } @JsonSerializable() -class Weather extends Object with _$WeatherSerializerMixin { +class Weather { Weather(this.icon, this.description, this.id, this.main); @JsonKey(name: 'icon') @@ -142,7 +142,7 @@ class Weather extends Object with _$WeatherSerializerMixin { } @JsonSerializable() -class Wind extends Object with _$WindSerializerMixin { +class Wind { Wind(this.deg, this.speed); @JsonKey(name: 'deg') diff --git a/example/lib/service/json/weather_in_cities.g.dart b/example/lib/service/json/weather_in_cities.g.dart index 3a602f5..a50731e 100644 --- a/example/lib/service/json/weather_in_cities.g.dart +++ b/example/lib/service/json/weather_in_cities.g.dart @@ -3,187 +3,136 @@ part of 'weather_in_cities.dart'; // ************************************************************************** -// Generator: JsonSerializableGenerator +// JsonSerializableGenerator // ************************************************************************** -WeatherInCities _$WeatherInCitiesFromJson(Map json) => - new WeatherInCities( - json['cnt'] as int, - (json['calctime'] as num)?.toDouble(), - json['cod'] as int, - (json['list'] as List) - ?.map((e) => - e == null ? null : new City.fromJson(e as Map)) - ?.toList()); - -abstract class _$WeatherInCitiesSerializerMixin { - int get cnt; - - double get calctime; - - int get cod; - - List get cities; - - Map toJson() => { - 'cnt': cnt, - 'calctime': calctime, - 'cod': cod, - 'list': cities - }; +WeatherInCities _$WeatherInCitiesFromJson(Map json) { + return WeatherInCities( + json['cnt'] as int, + (json['calctime'] as num).toDouble(), + json['cod'] as int, + (json['list'] as List) + .map((e) => City.fromJson(e as Map)) + .toList(), + ); } -City _$CityFromJson(Map json) => new City( +Map _$WeatherInCitiesToJson(WeatherInCities instance) => + { + 'cnt': instance.cnt, + 'calctime': instance.calctime, + 'cod': instance.cod, + 'list': instance.cities, + }; + +City _$CityFromJson(Map json) { + return City( json['id'] as int, - json['coord'] == null - ? null - : new Coord.fromJson(json['coord'] as Map), - json['clouds'] == null - ? null - : new Clouds.fromJson(json['clouds'] as Map), + Coord.fromJson(json['coord'] as Map), + Clouds.fromJson(json['clouds'] as Map), json['dt'] as int, json['name'] as String, - json['main'] == null - ? null - : new Main.fromJson(json['main'] as Map), + Main.fromJson(json['main'] as Map), json['rain'] == null ? null - : new Rain.fromJson(json['rain'] as Map), - (json['weather'] as List) - ?.map((e) => - e == null ? null : new Weather.fromJson(e as Map)) - ?.toList(), - json['wind'] == null - ? null - : new Wind.fromJson(json['wind'] as Map)); - -abstract class _$CitySerializerMixin { - int get id; - - Coord get coord; - - Clouds get clouds; - - int get dt; - - String get name; - - Main get main; - - Rain get rain; - - List get weather; - - Wind get wind; - - Map toJson() => { - 'id': id, - 'coord': coord, - 'clouds': clouds, - 'dt': dt, - 'name': name, - 'main': main, - 'rain': rain, - 'weather': weather, - 'wind': wind - }; + : Rain.fromJson(json['rain'] as Map), + (json['weather'] as List) + .map((e) => Weather.fromJson(e as Map)) + .toList(), + Wind.fromJson(json['wind'] as Map), + ); } -Coord _$CoordFromJson(Map json) => new Coord( - (json['Lat'] as num)?.toDouble(), (json['Lon'] as num)?.toDouble()); - -abstract class _$CoordSerializerMixin { - double get lat; - - double get lon; - - Map toJson() => {'Lat': lat, 'Lon': lon}; +Map _$CityToJson(City instance) => { + 'id': instance.id, + 'coord': instance.coord, + 'clouds': instance.clouds, + 'dt': instance.dt, + 'name': instance.name, + 'main': instance.main, + 'rain': instance.rain, + 'weather': instance.weather, + 'wind': instance.wind, + }; + +Coord _$CoordFromJson(Map json) { + return Coord( + (json['Lat'] as num).toDouble(), + (json['Lon'] as num).toDouble(), + ); } -Clouds _$CloudsFromJson(Map json) => - new Clouds(json['today'] as int); +Map _$CoordToJson(Coord instance) => { + 'Lat': instance.lat, + 'Lon': instance.lon, + }; -abstract class _$CloudsSerializerMixin { - int get today; - - Map toJson() => {'today': today}; +Clouds _$CloudsFromJson(Map json) { + return Clouds( + json['today'] as int, + ); } -Main _$MainFromJson(Map json) => new Main( - (json['sea_level'] as num)?.toDouble(), - json['humidity'] as int, - (json['grnd_level'] as num)?.toDouble(), - (json['pressure'] as num)?.toDouble(), - (json['temp_max'] as num)?.toDouble(), - (json['temp'] as num)?.toDouble(), - (json['temp_min'] as num)?.toDouble()); - -abstract class _$MainSerializerMixin { - double get seaLevel; - - int get humidity; - - double get grndLevel; - - double get pressure; +Map _$CloudsToJson(Clouds instance) => { + 'today': instance.today, + }; - double get tempMax; - - double get temp; - - double get tempMin; - - Map toJson() => { - 'sea_level': seaLevel, - 'humidity': humidity, - 'grnd_level': grndLevel, - 'pressure': pressure, - 'temp_max': tempMax, - 'temp': temp, - 'temp_min': tempMin - }; +Main _$MainFromJson(Map json) { + return Main( + (json['sea_level'] as num?)?.toDouble(), + json['humidity'] as int, + (json['grnd_level'] as num?)?.toDouble(), + (json['pressure'] as num).toDouble(), + (json['temp_max'] as num).toDouble(), + (json['temp'] as num).toDouble(), + (json['temp_min'] as num).toDouble(), + ); } -Rain _$RainFromJson(Map json) => - new Rain((json['3h'] as num)?.toDouble()); - -abstract class _$RainSerializerMixin { - double get threeH; - - Map toJson() => {'3h': threeH}; +Map _$MainToJson(Main instance) => { + 'sea_level': instance.seaLevel, + 'humidity': instance.humidity, + 'grnd_level': instance.grndLevel, + 'pressure': instance.pressure, + 'temp_max': instance.tempMax, + 'temp': instance.temp, + 'temp_min': instance.tempMin, + }; + +Rain _$RainFromJson(Map json) { + return Rain( + (json['3h'] as num).toDouble(), + ); } -Weather _$WeatherFromJson(Map json) => new Weather( +Map _$RainToJson(Rain instance) => { + '3h': instance.threeH, + }; + +Weather _$WeatherFromJson(Map json) { + return Weather( json['icon'] as String, json['description'] as String, json['id'] as int, - json['main'] as String); - -abstract class _$WeatherSerializerMixin { - String get icon; - - String get description; - - int get id; - - String get main; - - Map toJson() => { - 'icon': icon, - 'description': description, - 'id': id, - 'main': main - }; + json['main'] as String, + ); } -Wind _$WindFromJson(Map json) => new Wind( - (json['deg'] as num)?.toDouble(), (json['speed'] as num)?.toDouble()); - -abstract class _$WindSerializerMixin { - double get deg; - - double get speed; - - Map toJson() => - {'deg': deg, 'speed': speed}; +Map _$WeatherToJson(Weather instance) => { + 'icon': instance.icon, + 'description': instance.description, + 'id': instance.id, + 'main': instance.main, + }; + +Wind _$WindFromJson(Map json) { + return Wind( + (json['deg'] as num).toDouble(), + (json['speed'] as num).toDouble(), + ); } + +Map _$WindToJson(Wind instance) => { + 'deg': instance.deg, + 'speed': instance.speed, + }; diff --git a/example/lib/service/weather_entry.dart b/example/lib/service/weather_entry.dart index 27a6f2e..606df84 100644 --- a/example/lib/service/weather_entry.dart +++ b/example/lib/service/weather_entry.dart @@ -19,14 +19,15 @@ class WeatherEntry { : cityName = city.name, wind = city.wind.speed, temperature = city.main.temp, - description = city.weather[0]?.description, + description = city.weather[0].description, weatherId = city.weather[0].id; - @override - bool operator == (other) { - return cityName == other.cityName; - } + @override + bool operator ==(other) { + if (other is! WeatherEntry) return false; + return cityName == other.cityName; + } - @override - int get hashCode => cityName.hashCode; + @override + int get hashCode => cityName.hashCode; } diff --git a/example/lib/service/weather_service.dart b/example/lib/service/weather_service.dart index 77eac65..55c6755 100644 --- a/example/lib/service/weather_service.dart +++ b/example/lib/service/weather_service.dart @@ -1,32 +1,34 @@ import 'dart:async'; import 'dart:convert'; +import 'package:http/http.dart' as http; import 'package:rx_widget_demo/service/json/weather_in_cities.dart'; import 'package:rx_widget_demo/service/weather_entry.dart'; -import 'package:http/http.dart' as http; class WeatherService { - static String url = - "http://api.openweathermap.org/data/2.5/box/city?bbox=5,47,14,54,20&appid=27ac337102cc4931c24ba0b50aca6bbd"; - - final http.Client client; + static final url = 'https://api.openweathermap.org/data/2.5/box/' + 'city?bbox=12,32,15,37,10&appid=27ac337102cc4931c24ba0b50aca6bbd'; + static final uri = Uri.parse(url); WeatherService(this.client); - Future> getWeatherEntriesForCity(String filter) async { - final response = await client.get(url); + final http.Client client; + Future> getWeatherEntriesForCity(String? filter) async { + final response = await client.get(uri); + print(response.statusCode); if (response.statusCode == 200) { - return new WeatherInCities.fromJson(json.decode(response.body) as Map) + return WeatherInCities.fromJson( + json.decode(response.body) as Map) .cities .where((weatherInCity) => filter == null || filter.isEmpty || weatherInCity.name.toUpperCase().startsWith(filter.toUpperCase())) - .map((weatherInCity) => new WeatherEntry.from(weatherInCity)) + .map((weatherInCity) => WeatherEntry.from(weatherInCity)) .toList(); } else { - throw new Exception('No cities found'); + throw Exception(response.body); } } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b082950..223df25 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,73 +1,34 @@ name: rx_widget_demo description: A new Flutter project. +environment: + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.0 rxdart : any json_annotation: any - http: any - rx_command: ^5.0.1 + http: ^0.13.1 + rx_command: ^6.0.0-null-safety.3 rx_widgets: path: ../ dev_dependencies: test: any - mockito: any + mockito: ^5.0.5 quiver: any build_runner: any - json_serializable: any + json_serializable: 4.1.1 flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true fonts: - family: WeatherIcons fonts: - - asset: fonts/WeatherIcons.ttf - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + - asset: fonts/WeatherIcons.ttf \ No newline at end of file diff --git a/example/test/homepage_model_test.dart b/example/test/homepage_model_test.dart index ece98ff..8d3eeb7 100644 --- a/example/test/homepage_model_test.dart +++ b/example/test/homepage_model_test.dart @@ -1,4 +1,6 @@ import 'dart:async'; + +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:mockito/src/mock.dart'; import 'package:quiver/testing/async.dart'; @@ -8,29 +10,29 @@ import 'package:rx_widget_demo/service/weather_entry.dart'; import 'package:rx_widget_demo/service/weather_service.dart'; import 'package:test/test.dart'; -class MockService extends Mock implements WeatherService {} +import 'homepage_model_test.mocks.dart'; +// class MockService extends Mock implements WeatherService {} +@GenerateMocks([WeatherService]) main() { group('HomePageModel', () { test( 'should immediately fetch the weather with an empty string when the HomePageModel gets created', () async { - final service = MockService(); + final service = MockWeatherService(); when(service.getWeatherEntriesForCity(any)) - .thenAnswer((_) => Future.sync(() => [])); - final model = HomePageModel(service); // ignore: unused_local_variable - - + .thenAnswer((_) => Future.sync(() => [])); + final model = HomePageModel(service); - expect(model.updateWeatherCommand.results, emits(TypeMatcher())); + expect(model.updateWeatherCommand.results, + emits(TypeMatcher())); }); test('should not fetch if switch is off', () async { - final service = MockService(); - final model = HomePageModel(service); - + final service = MockWeatherService(); when(service.getWeatherEntriesForCity(any)) - .thenAnswer((_) => Future.sync(() => [])); + .thenAnswer((_) => Future.sync(() => [])); + final model = HomePageModel(service); model.switchChangedCommand(false); model.updateWeatherCommand('A'); @@ -39,15 +41,14 @@ main() { test('should filter after the user stops typing for 500ms', () async { // Use FakeAsync from the Quiver package to simulate time - FakeAsync().run((time) { - final service = MockService(); - final model = HomePageModel(service); - + FakeAsync().run((time) { + final service = MockWeatherService(); when(service.getWeatherEntriesForCity(any)) - .thenAnswer((_) => Future.sync(() => [])); + .thenAnswer((_) => Future.sync(() => [])); + final model = HomePageModel(service); model.textChangedCommand('A'); - time.elapse( Duration(milliseconds: 1000)); + time.elapse(Duration(milliseconds: 1000)); verify(service.getWeatherEntriesForCity('A')); }); @@ -55,15 +56,14 @@ main() { test('should not search if the user has not paused for 500ms', () async { // Use FakeAsync from the Quiver package to simulate time - FakeAsync().run((time) { - final service = MockService(); - final model = HomePageModel(service); - + FakeAsync().run((time) { + final service = MockWeatherService(); when(service.getWeatherEntriesForCity(any)) - .thenAnswer((_) => Future.sync(() => [])); + .thenAnswer((_) => Future.sync(() => [])); + final model = HomePageModel(service); model.textChangedCommand('A'); - time.elapse( Duration(milliseconds: 100)); + time.elapse(Duration(milliseconds: 100)); verifyNever(service.getWeatherEntriesForCity('A')); }); diff --git a/example/test/homepage_model_test.mocks.dart b/example/test/homepage_model_test.mocks.dart new file mode 100644 index 0000000..69d3da4 --- /dev/null +++ b/example/test/homepage_model_test.mocks.dart @@ -0,0 +1,35 @@ +// Mocks generated by Mockito 5.0.5 from annotations +// in rx_widget_demo/test/homepage_model_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:http/src/client.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:rx_widget_demo/service/weather_entry.dart' as _i5; +import 'package:rx_widget_demo/service/weather_service.dart' as _i3; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeClient extends _i1.Fake implements _i2.Client {} + +/// A class which mocks [WeatherService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWeatherService extends _i1.Mock implements _i3.WeatherService { + MockWeatherService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Client get client => (super.noSuchMethod(Invocation.getter(#client), + returnValue: _FakeClient()) as _i2.Client); + @override + _i4.Future> getWeatherEntriesForCity(String? filter) => + (super.noSuchMethod( + Invocation.method(#getWeatherEntriesForCity, [filter]), + returnValue: + Future>.value(<_i5.WeatherEntry>[])) + as _i4.Future>); +} diff --git a/example/test/homepage_test.dart b/example/test/homepage_test.dart index 0958142..a7ef869 100644 --- a/example/test/homepage_test.dart +++ b/example/test/homepage_test.dart @@ -1,56 +1,63 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:rx_command/rx_command.dart'; -import 'package:rx_widget_demo/homepage/homepage.dart'; +import 'package:rx_widget_demo/homepage/home_page.dart'; import 'package:rx_widget_demo/homepage/homepage_model.dart'; import 'package:rx_widget_demo/keys.dart'; import 'package:rx_widget_demo/model_provider.dart'; import 'package:rx_widget_demo/service/weather_entry.dart'; -import 'package:collection/collection.dart'; - import 'package:test/test.dart' as dart_test; +import 'homepage_test.mocks.dart'; - -class MockModel extends Mock implements HomePageModel {} +// class MockModel extends Mock implements HomePageModel {} //class MockCommand extends Mock implements RxCommand {} - -class MockStream extends Mock implements Stream{} - +// class MockStream extends Mock implements Stream {} +@GenerateMocks([HomePageModel]) main() { group('HomePage', () { - testWidgets('Shows a loading spinner and disables the button while executing and shows the ListView on data arrival', (tester) async { - final model = MockModel(); - final command = MockCommand>(); - final widget = ModelProvider( + testWidgets( + 'Shows a loading spinner and disables the button while ' + 'executing and shows the ListView on data arrival', (tester) async { + final model = MockHomePageModel(); + final command = MockCommand>(); + when(model.updateWeatherCommand).thenAnswer((_) => command); + + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp( + home: HomePage(), + ), ); - when(model.updateWeatherCommand).thenAnswer((_)=>command); + await tester.pumpWidget(widget); // Build initial State + await tester.pump(); -// model.updateWeatherCommand.canExecute.listen((b) => print("Can exceute: $b")); -// model.updateWeatherCommand.isExecuting.listen((b) => print("Is Exceuting: $b")); + final textFinder = find.text('WeatherDemo'); + expect(textFinder, findsOneWidget); - await tester.pumpWidget(widget);// Build initial State - await tester.pump(); + final keyFinder = find.byKey(AppKeys.textField); + expect(keyFinder, findsOneWidget); expect(find.byKey(AppKeys.loadingSpinner), findsNothing); expect(find.byKey(AppKeys.updateButtonDisabled), findsNothing); expect(find.byKey(AppKeys.updateButtonEnabled), findsOneWidget); + expect(find.byKey(AppKeys.widgetSelector), findsOneWidget); expect(find.byKey(AppKeys.weatherList), findsNothing); expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsOneWidget); - command.startExecution(); - await tester.pump(); - await tester.pump(); //because there are two streams involded it seems we have to pump twice so that both streambuilders can work + await tester.pump(); + //because there are two streams involved it seems we have to pump twice so that both stream builders can work + await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsOneWidget); expect(find.byKey(AppKeys.updateButtonDisabled), findsOneWidget); @@ -59,7 +66,8 @@ main() { expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); - command.endExecutionWithData([ WeatherEntry("London", 10.0, 30.0, "sunny", 12)]); + command.endExecutionWithData( + [WeatherEntry("London", 10.0, 30.0, "sunny", 12)]); await tester.pump(); // Build after Stream delivers value expect(find.byKey(AppKeys.loadingSpinner), findsNothing); @@ -70,22 +78,21 @@ main() { expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); }); - - testWidgets('shows place holder due to no data', (tester) async { - final model = MockModel(); - final command = MockCommand>(); - final widget = ModelProvider( + testWidgets('shows placeHolder due to no data', (tester) async { + final model = MockHomePageModel(); + final command = MockCommand>(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_)=>command); + when(model.updateWeatherCommand).thenAnswer((_) => command); - // model.updateWeatherCommand.canExecute.listen((b) => print("Can exceute: $b")); - // model.updateWeatherCommand.isExecuting.listen((b) => print("Is Exceuting: $b")); + // model.updateWeatherCommand.canExecute.listen((b) => print("Can exceute: $b")); + // model.updateWeatherCommand.isExecuting.listen((b) => print("Is Exceuting: $b")); - await tester.pumpWidget(widget);// Build initial State - await tester.pump(); + await tester.pumpWidget(widget); // Build initial State + await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsNothing); expect(find.byKey(AppKeys.updateButtonDisabled), findsNothing); @@ -94,10 +101,10 @@ main() { expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsOneWidget); - command.startExecution(); - await tester.pump(); - await tester.pump(); //because there are two streams involded it seems we have to pump twice so that both streambuilders can work + await tester.pump(); + //because there are two streams involved it seems we have to pump twice so that both stream builders can work + await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsOneWidget); expect(find.byKey(AppKeys.updateButtonDisabled), findsOneWidget); @@ -106,7 +113,7 @@ main() { expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); - command.endExecutionWithData(null); + command.endExecutionWithData([]); await tester.pump(); // Build after Stream delivers value expect(find.byKey(AppKeys.loadingSpinner), findsNothing); @@ -114,24 +121,24 @@ main() { expect(find.byKey(AppKeys.updateButtonEnabled), findsOneWidget); expect(find.byKey(AppKeys.weatherList), findsNothing); expect(find.byKey(AppKeys.loaderError), findsNothing); - expect(find.byKey(AppKeys.loaderPlaceHolder), findsOneWidget); + expect(find.text(noResultsText), findsOneWidget); }); testWidgets('Shows error view due to received error', (tester) async { - final model = MockModel(); - final command = MockCommand>(); - final widget = ModelProvider( + final model = MockHomePageModel(); + final command = MockCommand>(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_)=>command); + when(model.updateWeatherCommand).thenAnswer((_) => command); - // model.updateWeatherCommand.canExecute.listen((b) => print("Can exceute: $b")); - // model.updateWeatherCommand.isExecuting.listen((b) => print("Is Exceuting: $b")); + // model.updateWeatherCommand.canExecute.listen((b) => print("Can exceute: $b")); + // model.updateWeatherCommand.isExecuting.listen((b) => print("Is Exceuting: $b")); - await tester.pumpWidget(widget);// Build initial State - await tester.pump(); + await tester.pumpWidget(widget); // Build initial State + await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsNothing); expect(find.byKey(AppKeys.updateButtonDisabled), findsNothing); @@ -140,10 +147,10 @@ main() { expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsOneWidget); - command.startExecution(); - await tester.pump(); - await tester.pump(); //because there are two streams involded it seems we have to pump twice so that both streambuilders can work + await tester.pump(); + await tester + .pump(); //because there are two streams involded it seems we have to pump twice so that both streambuilders can work expect(find.byKey(AppKeys.loadingSpinner), findsOneWidget); expect(find.byKey(AppKeys.updateButtonDisabled), findsOneWidget); @@ -163,104 +170,109 @@ main() { expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); }); - - - testWidgets('Tapping update button updates the weather', (tester) async { - final model = MockModel(); - final command = MockCommand>(); - final widget = ModelProvider( + final model = MockHomePageModel(); + final command = MockCommand>(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_)=>command); - when(model.updateWeatherCommand).thenAnswer((_)=>command); + when(model.updateWeatherCommand).thenAnswer((_) => command); + when(model.updateWeatherCommand).thenAnswer((_) => command); - command.queueResultsForNextExecuteCall([CommandResult>( - [WeatherEntry("London", 10.0, 30.0, "sunny", 12)],null, false)]); + command.queueResultsForNextExecuteCall([ + CommandResult.data( + null, [WeatherEntry("London", 10.0, 30.0, "sunny", 12)]), + ]); - expect(command.results, dart_test.emitsInOrder([ crm([WeatherEntry("London", 10.0, 30.0, "sunny", 12)], false, false) ])); + expect( + command.results, + dart_test.emitsInOrder([ + crm([WeatherEntry("London", 10.0, 30.0, "sunny", 12)], false, false) + ])); - command.results.listen((data)=> print("Received: " + data.data.toString())); + command.results + .listen((data) => print("Received: " + data.data.toString())); await tester.pumpWidget(widget); // Build initial State await tester.pump(); // Build after Stream delivers value await tester.tap(find.byKey(AppKeys.updateButtonEnabled)); - - }); - testWidgets('calls updateWeatherCommand after text was entered in the textfield', (tester) async { - final model = MockModel(); - final commandUpdate = MockCommand>(); - final commandTextChange = MockCommand(); - final widget = ModelProvider( + testWidgets( + 'calls updateWeatherCommand after text was entered in the textfield', + (tester) async { + final model = MockHomePageModel(); + final commandUpdate = MockCommand>(); + final commandTextChange = MockCommand(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_)=>commandUpdate); //Allways needed because RxLoader binds to it - when(model.textChangedCommand).thenAnswer((_)=>commandTextChange); - + when(model.updateWeatherCommand).thenAnswer( + (_) => commandUpdate); //Allways needed because RxLoader binds to it + when(model.textChangedCommand).thenAnswer((_) => commandTextChange); + await tester.pumpWidget(widget); // Build initial State await tester.enterText(find.byKey(AppKeys.textField), 'London'); await tester.pump(); // Build after text entered await tester.tap(find.byKey(AppKeys.updateButtonEnabled)); - expect(commandTextChange.lastPassedValueToExecute,"London"); + expect(commandTextChange.lastPassedValueToExecute, "London"); }); + testWidgets('cannot tap update when commandUpdate is disabled', + (tester) async { + final model = MockHomePageModel(); + final commandUpdate = MockCommand>( + restriction: Stream.value(false)); - - testWidgets('cannot tap update when commandUpdate is disabled', (tester) async { - final model = MockModel(); - final commandUpdate = MockCommand>(canExecute: Stream.value(false)); - - final widget = ModelProvider( + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_)=>commandUpdate); //Allways needed because RxLoader binds to it - when(model.updateWeatherCommand).thenAnswer((_)=>commandUpdate); - + when(model.updateWeatherCommand).thenAnswer( + (_) => commandUpdate); //Allways needed because RxLoader binds to it + when(model.updateWeatherCommand).thenAnswer((_) => commandUpdate); await tester.pumpWidget(widget); // Build initial State await tester.pump(); // Build after Stream delivers value await tester.pump(); // Build after Stream delivers value - expect(find.byKey(AppKeys.updateButtonDisabled), findsOneWidget); // should display disabled button - expect(find.byKey(AppKeys.updateButtonEnabled), findsNothing); // should not display enabled button - + expect(find.byKey(AppKeys.updateButtonDisabled), + findsOneWidget); // should display disabled button + expect(find.byKey(AppKeys.updateButtonEnabled), + findsNothing); // should not display enabled button await tester.tap(find.byKey(AppKeys.updateButtonDisabled)); expect(commandUpdate.executionCount, 0); }); - - testWidgets('tapping switch toggles model', (tester) async { - final model = MockModel(); - final updateCommand = MockCommand>(canExecute: Stream.value(false)); - final switchCommand = MockCommand(); - final widget = ModelProvider( + final model = MockHomePageModel(); + final updateCommand = MockCommand>( + restriction: Stream.value(false)); + final switchCommand = MockCommand(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); when(model.updateWeatherCommand).thenAnswer((_) => updateCommand); when(model.switchChangedCommand).thenAnswer((_) => switchCommand); await tester.pumpWidget(widget); // Build initial State - await tester.pump(); + await tester.pump(); await tester.tap(find.byKey(AppKeys.updateSwitch)); // Starts out true, tapping should go false expect(switchCommand.lastPassedValueToExecute, false); - await tester.pump(); + await tester.pump(); // tap again await tester.tap(find.byKey(AppKeys.updateSwitch)); @@ -268,60 +280,55 @@ main() { expect(switchCommand.lastPassedValueToExecute, true); }); - - testWidgets('Tapping update button clears the filter field', (tester) async { - final model = MockModel(); - final command = MockCommand>(); - final widget = ModelProvider( + testWidgets('Tapping update button clears the filter field', + (tester) async { + final model = MockHomePageModel(); + final command = MockCommand>(); + final widget = ModelProvider( model: model, - child: MaterialApp(home: HomePage()), + child: MaterialApp(home: HomePage()), ); - when(model.updateWeatherCommand).thenAnswer((_) =>command); + when(model.updateWeatherCommand).thenAnswer((_) => command); + when(model.textChangedCommand).thenAnswer( + (Invocation realInvocation) => RxCommand.createSync((s) => s)); + const keyword = 'Tripoli'; await tester.pumpWidget(widget); // Build initial State - await tester.enterText(find.byKey(AppKeys.textField), 'London'); + await tester.pump(); + await tester.enterText(find.byKey(AppKeys.textField), keyword); + + final textField = tester.widget(find.byKey(AppKeys.textField)); + final controller = textField.controller!; + + expect(controller.text, keyword); await tester.pump(); // Build after Stream delivers value await tester.tap(find.byKey(AppKeys.updateButtonEnabled)); - - expect(tester.widget(find.byKey(AppKeys.textField)).controller.text.length, 0); + expect(controller.text.length, 0); }); - - - }); } +dart_test.StreamMatcher crm( + List data, bool hasError, bool isExecuting) { + return dart_test.StreamMatcher((x) async { + final event = await x.next as CommandResult>; + if (event.data != null) { + if (!ListEquality().equals(event.data, data)) { + return "Data not equal"; + } + } + if (!hasError && event.error != null) return "Had error while not expected"; + if (hasError && !(event.error is Exception)) return "Wong error type"; + if (event.isExecuting != isExecuting) + return "Wong isExecuting $isExecuting"; - - - dart_test.StreamMatcher crm(List data, bool hasError, bool isExceuting) - { - return dart_test.StreamMatcher((x) async { - var event = await x.next as CommandResult>; - if (event.data != null) - { - if (!ListEquality().equals(event.data, data)) - { - return "Data not equal"; - } - } - - if (!hasError && event.error != null) - return "Had error while not expected"; - - if (hasError && !(event.error is Exception)) - return "Wong error type"; - - if (event.isExecuting != isExceuting) - return "Wong isExecuting $isExceuting"; - - return null; - }, "Wrong value emmited:"); - } + return null; + }, "Wrong value emitted:"); +} diff --git a/example/test/homepage_test.mocks.dart b/example/test/homepage_test.mocks.dart new file mode 100644 index 0000000..0424e49 --- /dev/null +++ b/example/test/homepage_test.mocks.dart @@ -0,0 +1,45 @@ +// Mocks generated by Mockito 5.0.5 from annotations +// in rx_widget_demo/test/homepage_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:rx_command/rx_command.dart' as _i3; +import 'package:rx_widget_demo/homepage/homepage_model.dart' as _i4; +import 'package:rx_widget_demo/service/weather_entry.dart' as _i5; +import 'package:rx_widget_demo/service/weather_service.dart' as _i2; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeWeatherService extends _i1.Fake implements _i2.WeatherService {} + +class _FakeRxCommand extends _i1.Fake + implements _i3.RxCommand {} + +/// A class which mocks [HomePageModel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHomePageModel extends _i1.Mock implements _i4.HomePageModel { + MockHomePageModel() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WeatherService get service => + (super.noSuchMethod(Invocation.getter(#service), + returnValue: _FakeWeatherService()) as _i2.WeatherService); + @override + _i3.RxCommand> get updateWeatherCommand => + (super.noSuchMethod(Invocation.getter(#updateWeatherCommand), + returnValue: _FakeRxCommand>()) + as _i3.RxCommand>); + @override + _i3.RxCommand get switchChangedCommand => (super.noSuchMethod( + Invocation.getter(#switchChangedCommand), + returnValue: _FakeRxCommand()) as _i3.RxCommand); + @override + _i3.RxCommand get textChangedCommand => + (super.noSuchMethod(Invocation.getter(#textChangedCommand), + returnValue: _FakeRxCommand()) + as _i3.RxCommand); +} diff --git a/example/test/weather_service_test.dart b/example/test/weather_service_test.dart index fef897c..769dfaa 100644 --- a/example/test/weather_service_test.dart +++ b/example/test/weather_service_test.dart @@ -1,40 +1,40 @@ - import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:rx_widget_demo/service/weather_service.dart'; import 'package:test/test.dart'; -class MockClient extends Mock implements http.Client {} +import 'weather_service_test.mocks.dart'; + +// class MockClient extends Mock implements http.Client {} -main() { +@GenerateMocks([http.Client]) +void main() { group('WeatherService', () { test('should return all items when empty filter provided', () async { - final client = new MockClient(); - final service = new WeatherService(client); - when(client.get(WeatherService.url)) - .thenAnswer((_) async => new http.Response(responseBody, 200)); - + final client = MockClient(); + final service = WeatherService(client); + when(client.get(WeatherService.uri)) + .thenAnswer((_) async => http.Response(responseBody, 200)); final entries = await service.getWeatherEntriesForCity(''); - expect(entries.length, 3); }); test('should return all items when null filter provided', () async { - final client = new MockClient(); - final service = new WeatherService(client); - when(client.get(WeatherService.url)) - .thenAnswer((_) async => new http.Response(responseBody, 200)); - + final client = MockClient(); + final service = WeatherService(client); + when(client.get(WeatherService.uri)) + .thenAnswer((_) async => http.Response(responseBody, 200)); final entries = await service.getWeatherEntriesForCity(null); expect(entries.length, 3); }); test('should filter the entries when given a city name', () async { - final client = new MockClient(); - final service = new WeatherService(client); - when(client.get(WeatherService.url)) - .thenAnswer((_) async => new http.Response(responseBody, 200)); + final client = MockClient(); + final service = WeatherService(client); + when(client.get(WeatherService.uri)) + .thenAnswer((_) async => http.Response(responseBody, 200)); final entries = await service.getWeatherEntriesForCity('Dole'); @@ -42,19 +42,19 @@ main() { }); test('should throw an exception when the response is not 200', () async { - final client = new MockClient(); - final service = new WeatherService(client); - when(client.get(WeatherService.url)) - .thenAnswer((_) async => new http.Response('Error', 404)); + final client = MockClient(); + final service = WeatherService(client); + when(client.get(WeatherService.uri)) + .thenAnswer((_) async => http.Response('Error', 404)); expect(service.getWeatherEntriesForCity('Dole'), throwsException); }); test('should throw an exception when the json is malformed', () async { - final client = new MockClient(); - final service = new WeatherService(client); - when(client.get(WeatherService.url)) - .thenAnswer((_) async => new http.Response('p[2p[1p[ppadsdaf', 200)); + final client = MockClient(); + final service = WeatherService(client); + when(client.get(WeatherService.uri)) + .thenAnswer((_) async => http.Response('p[2p[1p[ppadsdaf', 200)); expect(service.getWeatherEntriesForCity('Dole'), throwsException); }); diff --git a/example/test/weather_service_test.mocks.dart b/example/test/weather_service_test.mocks.dart new file mode 100644 index 0000000..97966b6 --- /dev/null +++ b/example/test/weather_service_test.mocks.dart @@ -0,0 +1,102 @@ +// Mocks generated by Mockito 5.0.5 from annotations +// in rx_widget_demo/test/weather_service_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i6; +import 'dart:convert' as _i7; +import 'dart:typed_data' as _i3; + +import 'package:http/src/base_request.dart' as _i8; +import 'package:http/src/client.dart' as _i5; +import 'package:http/src/response.dart' as _i2; +import 'package:http/src/streamed_response.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeResponse extends _i1.Fake implements _i2.Response {} + +class _FakeUint8List extends _i1.Fake implements _i3.Uint8List {} + +class _FakeStreamedResponse extends _i1.Fake implements _i4.StreamedResponse {} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i5.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future<_i2.Response> head(Uri? url, {Map? headers}) => + (super.noSuchMethod(Invocation.method(#head, [url], {#headers: headers}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future<_i2.Response> get(Uri? url, {Map? headers}) => + (super.noSuchMethod(Invocation.method(#get, [url], {#headers: headers}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future<_i2.Response> post(Uri? url, + {Map? headers, + Object? body, + _i7.Encoding? encoding}) => + (super.noSuchMethod( + Invocation.method(#post, [url], + {#headers: headers, #body: body, #encoding: encoding}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future<_i2.Response> put(Uri? url, + {Map? headers, + Object? body, + _i7.Encoding? encoding}) => + (super.noSuchMethod( + Invocation.method(#put, [url], + {#headers: headers, #body: body, #encoding: encoding}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future<_i2.Response> patch(Uri? url, + {Map? headers, + Object? body, + _i7.Encoding? encoding}) => + (super.noSuchMethod( + Invocation.method(#patch, [url], + {#headers: headers, #body: body, #encoding: encoding}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future<_i2.Response> delete(Uri? url, + {Map? headers, + Object? body, + _i7.Encoding? encoding}) => + (super.noSuchMethod( + Invocation.method(#delete, [url], + {#headers: headers, #body: body, #encoding: encoding}), + returnValue: Future<_i2.Response>.value(_FakeResponse())) + as _i6.Future<_i2.Response>); + @override + _i6.Future read(Uri? url, {Map? headers}) => + (super.noSuchMethod(Invocation.method(#read, [url], {#headers: headers}), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future<_i3.Uint8List> readBytes(Uri? url, + {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#readBytes, [url], {#headers: headers}), + returnValue: Future<_i3.Uint8List>.value(_FakeUint8List())) + as _i6.Future<_i3.Uint8List>); + @override + _i6.Future<_i4.StreamedResponse> send(_i8.BaseRequest? request) => + (super.noSuchMethod(Invocation.method(#send, [request]), + returnValue: + Future<_i4.StreamedResponse>.value(_FakeStreamedResponse())) + as _i6.Future<_i4.StreamedResponse>); + @override + void close() => super.noSuchMethod(Invocation.method(#close, []), + returnValueForMissingStub: null); +} diff --git a/lib/src/reactive_base_widget.dart b/lib/src/reactive_base_widget.dart index 137d2bb..1f60628 100644 --- a/lib/src/reactive_base_widget.dart +++ b/lib/src/reactive_base_widget.dart @@ -1,21 +1,21 @@ import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; abstract class ReactiveBaseWidget extends StatefulWidget { final Stream stream; - final T initialData; + final T? initialData; @mustCallSuper - const ReactiveBaseWidget(this.stream, this.initialData, {Key key}) - : assert(stream != null), - super(key: key); + const ReactiveBaseWidget(this.stream, this.initialData, {Key? key}) : super(key: key); Widget build(BuildContext context, T data); + Widget errorBuild(BuildContext context, Object error) { return Center( - child: Text(error), + child: Text(error.toString()), ); } @@ -35,9 +35,8 @@ class _ReactiveBaseWidgetState extends State> { initialData: widget.initialData, stream: widget.stream, builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) - return widget.errorBuild(context, snapshot.error); - if (snapshot.hasData) return widget.build(context, snapshot.data); + if (snapshot.hasError) return widget.errorBuild(context, snapshot.error!); + if (snapshot.hasData) return widget.build(context, snapshot.data!); return widget.placeHolderBuild(context); }, ); diff --git a/lib/src/reactive_builder.dart b/lib/src/reactive_builder.dart index a331a4f..3a45460 100644 --- a/lib/src/reactive_builder.dart +++ b/lib/src/reactive_builder.dart @@ -5,14 +5,14 @@ import 'reactive_base_widget.dart'; class ReactiveBuilder extends ReactiveBaseWidget { final RxBuilder builder; - final ErrorBuilder errorBuilder; - final PlaceHolderBuilder placeHolderBuilder; + final ErrorBuilder? errorBuilder; + final PlaceHolderBuilder? placeHolderBuilder; const ReactiveBuilder({ - Key key, - @required Stream stream, - T initialData, - @required this.builder, + Key? key, + required Stream stream, + T? initialData, + required this.builder, this.placeHolderBuilder, this.errorBuilder, }) : super(stream, initialData, key: key); @@ -22,13 +22,13 @@ class ReactiveBuilder extends ReactiveBaseWidget { @override Widget placeHolderBuild(BuildContext context) { - if (placeHolderBuilder != null) return placeHolderBuilder(context); + if (placeHolderBuilder != null) return placeHolderBuilder!(context); return super.placeHolderBuild(context); } @override Widget errorBuild(BuildContext context, Object error) { - if (errorBuilder != null) return errorBuilder(context, error); + if (errorBuilder != null) return errorBuilder!(context, error); return super.errorBuild(context, error); } } diff --git a/lib/src/reactive_widget.dart b/lib/src/reactive_widget.dart index 151877b..d86d18b 100644 --- a/lib/src/reactive_widget.dart +++ b/lib/src/reactive_widget.dart @@ -5,32 +5,30 @@ import 'reactive_base_widget.dart'; class ReactiveWidget extends ReactiveBaseWidget { final RxWidget widget; - final RxErrorWidget errorWidget; - final Widget placeHolderWidget; + final RxErrorWidget? errorWidget; + final Widget? placeHolderWidget; const ReactiveWidget({ - Key key, - @required Stream stream, - T initialData, - @required this.widget, + Key? key, + required Stream stream, + T? initialData, + required this.widget, this.placeHolderWidget, this.errorWidget, - }) : assert(stream != null), - assert(widget != null), - super(stream, initialData, key: key); + }) : super(stream, initialData, key: key); @override Widget build(BuildContext context, T data) => widget(data); @override Widget placeHolderBuild(BuildContext context) { - if (placeHolderWidget != null) return placeHolderWidget; + if (placeHolderWidget != null) return placeHolderWidget!; return super.placeHolderBuild(context); } @override Widget errorBuild(BuildContext context, Object error) { - if (errorWidget != null) return errorWidget(error); + if (errorWidget != null) return errorWidget!(error); return super.errorBuild(context, error); } } diff --git a/lib/src/rx_command_builder.dart b/lib/src/rx_command_builder.dart index e7887a2..bc52b11 100644 --- a/lib/src/rx_command_builder.dart +++ b/lib/src/rx_command_builder.dart @@ -4,20 +4,20 @@ import 'package:flutter/material.dart'; import 'package:rx_command/rx_command.dart'; import 'package:rx_widgets/src/builder_functions.dart'; -/// Spinner/Busy indicator that reacts on the output of a `Stream>`. +/// Spinner/Busy indicator that reacts on the output of a `Stream>`. /// It's made especially to work together with `RxCommand` from the `rx_command`package. /// it starts running as soon as an item with `isExecuting==true` is received until `isExecuting==true` is received. /// To react on other possible states (`data, no data, error`) that can be emitted it offers three option `Builder` methods. -class RxCommandBuilder extends StatelessWidget { - final Stream> commandResults; - final RxBuilder dataBuilder; - final ErrorBuilder errorBuilder; - final BusyBuilder busyBuilder; - final PlaceHolderBuilder placeHolderBuilder; - final TargetPlatform platform; +class RxCommandBuilder extends StatelessWidget { + final Stream> commandResults; + final RxBuilder? dataBuilder; + final ErrorBuilder>? errorBuilder; + final BusyBuilder? busyBuilder; + final PlaceHolderBuilder? placeHolderBuilder; + final TargetPlatform? platform; /// Creates a new `RxCommandBuilder` instance - /// [commandResults] : `Stream>` or a `RxCommand` that issues `CommandResults` + /// [commandResults] : `Stream>` or a `RxCommand` that issues `CommandResults` /// [busyBuilder] : Builder that will be called as soon as an event with `isExecuting==true`. /// [dataBuilder] : Builder that will be called as soon as an event with data is received. It will get passed the `data` field of the CommandResult. /// If this is null a `Container` will be created instead. @@ -26,42 +26,39 @@ class RxCommandBuilder extends StatelessWidget { /// [dataBuilder] : Builder that will be called as soon as an event with an `error` is received. It will get passed the `error` field of the CommandResult. /// If this is null a `Container` will be created instead. const RxCommandBuilder({ - Key key, - @required this.commandResults, + Key? key, + required this.commandResults, this.platform, this.busyBuilder, this.dataBuilder, this.placeHolderBuilder, this.errorBuilder, - }) : assert(commandResults != null), - super(key: key); + }) : super(key: key); @override Widget build(BuildContext context) { - return StreamBuilder>( + return StreamBuilder>( stream: commandResults, builder: (context, snapshot) { - CommandResult item; + CommandResult? item; if (snapshot.hasData) { item = snapshot.data; } else if (snapshot.hasError) { - item = CommandResult.error(snapshot.error); + item = CommandResult.error(null, snapshot.error); } else { - item = const CommandResult(null, null, false); + item = const CommandResult.blank(); } - return _processItem(context, item); + return _processItem(context, item!); }, ); } - Widget _processItem(BuildContext context, CommandResult item) { - assert(item != null); - + Widget _processItem(BuildContext context, CommandResult item) { if (item.isExecuting) { if (busyBuilder != null) { - return busyBuilder(context); + return busyBuilder!(context); } else { - final spinner = (platform ?? defaultTargetPlatform == TargetPlatform.iOS) + final spinner = ((platform ?? defaultTargetPlatform) == TargetPlatform.iOS) ? const CupertinoActivityIndicator() : const CircularProgressIndicator(); return Center(child: spinner); @@ -70,7 +67,7 @@ class RxCommandBuilder extends StatelessWidget { if (item.hasData) { if (dataBuilder != null) { - return dataBuilder(context, item.data); + return dataBuilder!(context, item.data!); } else { return const SizedBox(); } @@ -78,14 +75,15 @@ class RxCommandBuilder extends StatelessWidget { if (item.hasError) { if (errorBuilder != null) { - return errorBuilder(context, item.error); + final commandError = item.error is CommandError ? item.error! : CommandError(item.paramData, item.error); + return errorBuilder!(context, commandError); } else { return const SizedBox(); } } if (placeHolderBuilder != null) { - return placeHolderBuilder(context); + return placeHolderBuilder!(context); } else { return const SizedBox(); } diff --git a/lib/src/rx_command_handler_mixin.dart b/lib/src/rx_command_handler_mixin.dart index 793630d..018555f 100644 --- a/lib/src/rx_command_handler_mixin.dart +++ b/lib/src/rx_command_handler_mixin.dart @@ -11,7 +11,8 @@ mixin RxCommandHandlerMixin on StatelessWidget { final _state = _MixinState(); @override - StatelessElement createElement() => _StatelessMixInElement(this); + StatelessElement createElement() => + _StatelessMixInElement(this); RxCommandListener get commandListener; } @@ -25,19 +26,21 @@ mixin RxCommandStatefulHandlerMixin on StatefulWidget { final _state = _MixinState(); @override - StatefulElement createElement() => _StatefulMixInElement(this); + StatefulElement createElement() => + _StatefulMixInElement(this); RxCommandListener get commandListener; } -class _StatelessMixInElement extends StatelessElement { +class _StatelessMixInElement + extends StatelessElement { _StatelessMixInElement(W widget) : super(widget); @override - W get widget => super.widget; + W get widget => super.widget as W; @override - void mount(Element parent, newSlot) { + void mount(Element? parent, newSlot) { widget._state.init(widget.commandListener); super.mount(parent, newSlot); } @@ -49,15 +52,15 @@ class _StatelessMixInElement extends Stat } } - -class _StatefulMixInElement extends StatefulElement { +class _StatefulMixInElement + extends StatefulElement { _StatefulMixInElement(W widget) : super(widget); @override - W get widget => super.widget; + W get widget => super.widget as W; @override - void mount(Element parent, newSlot) { + void mount(Element? parent, newSlot) { widget._state.init(widget.commandListener); super.mount(parent, newSlot); } @@ -69,11 +72,10 @@ class _StatefulMixInElement exten } } - class _MixinState { - RxCommandListener _listener; + RxCommandListener? _listener; void init(RxCommandListener listener) => _listener = listener; void dispose() => _listener?.dispose(); -} \ No newline at end of file +} diff --git a/lib/src/rx_raised_button.dart b/lib/src/rx_raised_button.dart index a11e2ed..896975d 100644 --- a/lib/src/rx_raised_button.dart +++ b/lib/src/rx_raised_button.dart @@ -5,28 +5,29 @@ import 'package:rx_command/rx_command.dart'; /// so the button gets disabled if the `rxCommand` has the `canExecute` set to `false` or when it is executing class RxRaisedButton extends StatelessWidget { final RxCommand rxCommand; - final ValueChanged onHighlightChanged; - final ButtonTextTheme textTheme; - final Color textColor; - final Color disabledTextColor; - final Color color; - final Color disabledColor; - final Color highlightColor; - final Color splashColor; - final Brightness colorBrightness; - final double elevation; - final double highlightElevation; - final double disabledElevation; - final EdgeInsetsGeometry padding; - final ShapeBorder shape; + final ValueChanged? onHighlightChanged; + final ButtonTextTheme? textTheme; + final Color? textColor; + final Color? disabledTextColor; + final Color? color; + final Color? disabledColor; + final Color? highlightColor; + final Color? splashColor; + final Brightness? colorBrightness; + final double? elevation; + final double? highlightElevation; + final double? disabledElevation; + final EdgeInsetsGeometry? padding; + final ShapeBorder? shape; final Clip clipBehavior = Clip.none; - final MaterialTapTargetSize materialTapTargetSize; - final Duration animationDuration; - final Widget child; + final MaterialTapTargetSize? materialTapTargetSize; + final Duration? animationDuration; + final Widget? child; + RxRaisedButton({ - Key key, + Key? key, this.child, - this.rxCommand, + required this.rxCommand, this.onHighlightChanged, this.textTheme, this.textColor, @@ -44,13 +45,15 @@ class RxRaisedButton extends StatelessWidget { this.materialTapTargetSize, this.animationDuration, }) : super(key: key); + @override Widget build(BuildContext context) { return StreamBuilder( stream: rxCommand.canExecute, builder: (context, snapshot) { + // ignore: deprecated_member_use return RaisedButton( - onPressed: snapshot.data ? rxCommand : null, + onPressed: snapshot.hasData ? () => rxCommand() : null, onHighlightChanged: onHighlightChanged, textTheme: textTheme, textColor: textColor, diff --git a/lib/src/rx_spinner.dart b/lib/src/rx_spinner.dart index 3947106..acb08ae 100644 --- a/lib/src/rx_spinner.dart +++ b/lib/src/rx_spinner.dart @@ -1,11 +1,12 @@ import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:rx_command/rx_command.dart'; import 'package:rx_widgets/src/builder_functions.dart'; import 'package:rx_widgets/src/widget_selector.dart'; -import 'package:flutter/material.dart'; /// Spinner/Busy Indicator that reacts on the output of a `Stream` it starts running as soon as a `true` value is received /// until the next `false`is emitted. If the Spinner should replace another Widget while Spinning this widget can be passed as `normal` parameter. @@ -13,16 +14,16 @@ import 'package:flutter/material.dart'; /// Needless to say that `RxSpinner` is ideal in combination with `RxCommand's` `isExecuting` Observable class RxSpinner extends StatelessWidget { final Stream busyEvents; - final Widget normal; + final Widget? normal; - final TargetPlatform platform; + final TargetPlatform? platform; final double radius; - final Color backgroundColor; - final Animation valueColor; + final Color? backgroundColor; + final Animation? valueColor; final double strokeWidth; - final double value; + final double? value; /// Creates a new RxSpinner instance /// `busyEvents` : `Stream` that controls the activity of the Spinner. On receiving `true` it replaces the `normal` widget @@ -33,7 +34,7 @@ class RxSpinner extends StatelessWidget { /// all other parameters please see https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html /// they are ignored if the platform style is iOS. const RxSpinner( - {this.busyEvents, + {required this.busyEvents, this.platform, this.radius = 20.0, this.backgroundColor, @@ -41,9 +42,8 @@ class RxSpinner extends StatelessWidget { this.valueColor, this.strokeWidth: 4.0, this.normal, - Key key}) - : assert(busyEvents != null), - super(key: key); + Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -62,42 +62,35 @@ class RxSpinner extends StatelessWidget { return WidgetSelector( buildEvents: busyEvents, - onTrue: Center( - child: Container( - width: this.radius * 2, height: this.radius * 2, child: spinner)), + onTrue: Center(child: Container(width: this.radius * 2, height: this.radius * 2, child: spinner)), onFalse: normal != null ? normal : Container(), ); } } -/* -typedef BuilderFunction = Widget Function(BuildContext context, T data); -typedef BuilderFunction1 = Widget Function(BuildContext context); -*/ -/// Spinner/Busyindicator that reacts on the output of a `Stream>`. It's made especially to work together with -/// `RxCommand` from the `rx_command`package. -/// it starts running as soon as an item with `isExecuting==true` is received -/// until `isExecuting==true` is received. -/// To react on other possible states (`data, nodata, error`) that can be emitted it offers three option `Builder` methods -class RxLoader extends StatefulWidget { - final Stream> commandResults; - final RxBuilder dataBuilder; - final ErrorBuilder errorBuilder; - final PlaceHolderBuilder placeHolderBuilder; - - final TargetPlatform platform; +/// Spinner/Busy indicator that reacts on the output of a `Stream>`. +/// It's made especially to work together with `RxCommand` from the `rx_command`package. +/// It starts running as soon as an item with `isExecuting==true` is received until `isExecuting==true` is received. +/// To react on other possible states (`data, no data, error`) that can be emitted it offers three option `Builder` methods +class RxLoader extends StatefulWidget { + final Stream> commandResults; + final RxBuilder? dataBuilder; + final ErrorBuilder? errorBuilder; + final PlaceHolderBuilder? placeHolderBuilder; + + final TargetPlatform? platform; final double radius; - final Color backgroundColor; - final Animation valueColor; + final Color? backgroundColor; + final Animation? valueColor; final double strokeWidth; - final double value; + final double? value; - final Key spinnerKey; + final Key? spinnerKey; /// Creates a new `RxLoader` instance - /// [commandResults] : `Stream>` or a `RxCommand` that issues `CommandResults` + /// [commandResults] : `Stream>` or a `RxCommand` that issues `CommandResults` /// [platform] : defines platform style of the Spinner. If this is null or not provided the style of the current platform will be used /// [radius] : radius of the Spinner /// [dataBuilder] : Builder that will be called as soon as an event with data is received. It will get passed the `data` feeld of the CommandResult. @@ -110,9 +103,9 @@ class RxLoader extends StatefulWidget { /// all other parameters please see https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html /// they are ignored if the platform style is iOS. const RxLoader({ - Key key, + Key? key, this.spinnerKey, - this.commandResults, + required this.commandResults, this.platform, this.radius = 20.0, this.backgroundColor, @@ -122,21 +115,20 @@ class RxLoader extends StatefulWidget { this.dataBuilder, this.placeHolderBuilder, this.errorBuilder, - }) : assert(commandResults != null), - super(key: key); + }) : super(key: key); @override _RxLoaderState createState() { - return _RxLoaderState(commandResults); + return _RxLoaderState(commandResults); } } -class _RxLoaderState extends State> { - StreamSubscription> subscription; +class _RxLoaderState extends State> { + StreamSubscription>? subscription; - Stream> commandResults; + Stream> commandResults; - CommandResult lastReceivedItem = CommandResult(null, null, false); + CommandResult lastReceivedItem = CommandResult(null, null, null, false); _RxLoaderState(this.commandResults); @@ -151,7 +143,7 @@ class _RxLoaderState extends State> { } @override - void didUpdateWidget(RxLoader oldWidget) { + void didUpdateWidget(RxLoader oldWidget) { super.didUpdateWidget(oldWidget); subscription?.cancel(); @@ -170,8 +162,7 @@ class _RxLoaderState extends State> { @override Widget build(BuildContext context) { - var platformToUse = - widget.platform != null ? widget.platform : defaultTargetPlatform; + var platformToUse = widget.platform != null ? widget.platform : defaultTargetPlatform; var spinner = (platformToUse == TargetPlatform.iOS) ? CupertinoActivityIndicator( @@ -186,27 +177,23 @@ class _RxLoaderState extends State> { value: widget.value, ); if (lastReceivedItem.isExecuting) { - return Center( - child: Container( - width: this.widget.radius * 2, - height: this.widget.radius * 2, - child: spinner)); + return Center(child: Container(width: this.widget.radius * 2, height: this.widget.radius * 2, child: spinner)); } if (lastReceivedItem.hasData) { if (widget.dataBuilder != null) { - return widget.dataBuilder(context, lastReceivedItem.data); + return widget.dataBuilder!(context, lastReceivedItem.data!); } } if (!lastReceivedItem.hasData && !lastReceivedItem.hasError) { if (widget.placeHolderBuilder != null) { - return widget.placeHolderBuilder(context); + return widget.placeHolderBuilder!(context); } } if (lastReceivedItem.hasError) { if (widget.errorBuilder != null) { - return widget.errorBuilder(context, lastReceivedItem.error); + return widget.errorBuilder!(context, lastReceivedItem.error!); } } diff --git a/lib/src/rx_text.dart b/lib/src/rx_text.dart index 6a743dc..3a597dd 100644 --- a/lib/src/rx_text.dart +++ b/lib/src/rx_text.dart @@ -5,12 +5,11 @@ import 'package:flutter/material.dart'; import 'builder_functions.dart'; import 'reactive_base_widget.dart'; - /// A reimplementation of `Text` so it takes a [Stream] instead of `String` as data /// and reacts on it. class RxText extends ReactiveBaseWidget { - final ErrorBuilder errorBuilder; - final PlaceHolderBuilder placeHolderBuilder; + final ErrorBuilder? errorBuilder; + final PlaceHolderBuilder? placeHolderBuilder; /// The text to display as a [TextSpan]. /// If non-null, the style to use for this text. @@ -18,10 +17,10 @@ class RxText extends ReactiveBaseWidget { /// If the style's "inherit" property is true, the style will be merged with /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will /// replace the closest enclosing [DefaultTextStyle]. - final TextStyle style; + final TextStyle? style; /// How the text should be aligned horizontally. - final TextAlign textAlign; + final TextAlign? textAlign; /// The directionality of the text. /// @@ -36,7 +35,7 @@ class RxText extends ReactiveBaseWidget { /// its left. /// /// Defaults to the ambient [Directionality], if any. - final TextDirection textDirection; + final TextDirection? textDirection; /// Used to select a font when the same Unicode character can /// be rendered differently, depending on the locale. @@ -45,15 +44,15 @@ class RxText extends ReactiveBaseWidget { /// is inherited from the enclosing app with `Localizations.localeOf(context)`. /// /// See Flutter RenderParagraph.locale for more information. - final Locale locale; + final Locale? locale; /// Whether the text should break at soft line breaks. /// /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. - final bool softWrap; + final bool? softWrap; /// How visual overflow should be handled. - final TextOverflow overflow; + final TextOverflow? overflow; /// The number of font pixels for each logical pixel. /// @@ -63,7 +62,7 @@ class RxText extends ReactiveBaseWidget { /// The value given to the constructor as textScaleFactor. If null, will /// use the [MediaQueryData.textScaleFactor] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. - final double textScaleFactor; + final double? textScaleFactor; /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according @@ -76,7 +75,7 @@ class RxText extends ReactiveBaseWidget { /// an explicit number for its [DefaultTextStyle.maxLines], then the /// [DefaultTextStyle] value will take precedence. You can use a [RichText] /// widget directly to entirely override the [DefaultTextStyle]. - final int maxLines; + final int? maxLines; /// An alternative semantics label for this text. /// @@ -90,12 +89,12 @@ class RxText extends ReactiveBaseWidget { /// Text(r'$$', semanticsLabel: 'Double dollars') /// /// ``` - final String semanticsLabel; + final String? semanticsLabel; RxText( Stream stream, { - Key key, - String initialData, + Key? key, + String? initialData, this.errorBuilder, this.placeHolderBuilder, this.style, @@ -132,13 +131,13 @@ class RxText extends ReactiveBaseWidget { @override Widget errorBuild(BuildContext context, Object error) { - if (errorBuilder != null) return errorBuilder(context, error); + if (errorBuilder != null) return errorBuilder!(context, error); return Container(); } @override Widget placeHolderBuild(BuildContext context) { - if (placeHolderBuilder != null) return placeHolderBuilder(context); + if (placeHolderBuilder != null) return placeHolderBuilder!(context); return Container(); } } diff --git a/lib/src/widget_selector.dart b/lib/src/widget_selector.dart index c0af02c..621e9c6 100644 --- a/lib/src/widget_selector.dart +++ b/lib/src/widget_selector.dart @@ -9,28 +9,25 @@ import 'reactive_base_widget.dart'; /// This is pretty handy if you want to react to state change like enable/disable in you ViewModel and update /// the View accordingly. /// If you don't need builders for the alternative child widgets this class offers a more concise expression than `WidgetBuilderSelector` - class WidgetSelector extends ReactiveBaseWidget { - final Widget onTrue; - final Widget onFalse; - final ErrorBuilder errorBuilder; - final PlaceHolderBuilder placeHolderBuilder; + final Widget? onTrue; + final Widget? onFalse; + final ErrorBuilder? errorBuilder; + final PlaceHolderBuilder? placeHolderBuilder; /// Creates a new WidgetSelector instance /// `stream` : `Stream`that signals that the this Widget should be updated /// `onTrue` : Widget that should be returned if an item with value true is received /// `onFalse`: Widget that should be returned if an item with value false is received - const WidgetSelector({ - Key key, - Stream buildEvents, + Key? key, + required Stream buildEvents, this.onTrue, this.onFalse, this.errorBuilder, this.placeHolderBuilder, - bool initialValue, - }) : assert(buildEvents != null), - super(buildEvents, initialValue, key: key); + bool? initialValue, + }) : super(buildEvents, initialValue, key: key); @override Widget build(BuildContext context, data) { @@ -40,13 +37,13 @@ class WidgetSelector extends ReactiveBaseWidget { @override Widget placeHolderBuild(BuildContext context) { - if (placeHolderBuilder != null) return placeHolderBuilder(context); + if (placeHolderBuilder != null) return placeHolderBuilder!(context); return onFalse ?? SizedBox(); } @override Widget errorBuild(BuildContext context, Object error) { - if (errorBuilder != null) return errorBuilder(context, error); + if (errorBuilder != null) return errorBuilder!(context, error); return onFalse ?? SizedBox(); } } @@ -57,29 +54,28 @@ class WidgetSelector extends ReactiveBaseWidget { /// In comparison to `WidgetSelector` this is best used if the alternative child widgets are large so that you don't want to have them created /// without using them. class WidgetBuilderSelector extends ReactiveBaseWidget { - final WidgetBuilder onTrue; - final WidgetBuilder onFalse; - final ErrorBuilder errorBuilder; - final PlaceHolderBuilder placeHolderBuilder; + final WidgetBuilder? onTrue; + final WidgetBuilder? onFalse; + final ErrorBuilder? errorBuilder; + final PlaceHolderBuilder? placeHolderBuilder; /// Creates a new WidgetBuilderSelector instance /// `stream` : `Stream`that signals that the this Widget should be updated /// `onTrue` : builder that should be executed if an item with value true is received /// `onFalse`: builder that should be executed if an item with value false is received const WidgetBuilderSelector({ - Stream buildEvents, + required Stream buildEvents, this.onTrue, this.onFalse, this.errorBuilder, this.placeHolderBuilder, - Key key, - bool initialValue, - }) : assert(buildEvents != null), - super(buildEvents, initialValue, key: key); + Key? key, + bool? initialValue, + }) : super(buildEvents, initialValue, key: key); Widget onTrueWidget(BuildContext context) { if (onTrue != null) { - return onTrue(context); + return onTrue!(context); } else { return SizedBox(); } @@ -87,7 +83,7 @@ class WidgetBuilderSelector extends ReactiveBaseWidget { Widget onFalseWidget(BuildContext context) { if (onFalse != null) { - return onFalse(context); + return onFalse!(context); } else { return SizedBox(); } @@ -101,13 +97,13 @@ class WidgetBuilderSelector extends ReactiveBaseWidget { @override Widget placeHolderBuild(BuildContext context) { - if (placeHolderBuilder != null) return placeHolderBuilder(context); + if (placeHolderBuilder != null) return placeHolderBuilder!(context); return onFalseWidget(context); } @override Widget errorBuild(BuildContext context, Object error) { - if (errorBuilder != null) return errorBuilder(context, error); + if (errorBuilder != null) return errorBuilder!(context, error); return onFalseWidget(context); } } diff --git a/pubspec.yaml b/pubspec.yaml index 3e65d65..0590409 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,13 +5,13 @@ author: Thomas Burkhart homepage: https://github.com/escamoteur/rx_widgets documentation: environment: - sdk: ">=2.1.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - rx_command: ^5.0.1 + rx_command: ^6.0.0-null-safety.3 dev_dependencies: flutter_test: diff --git a/test/reactive_builder_test.dart b/test/reactive_builder_test.dart index fc10e69..b3d0b01 100644 --- a/test/reactive_builder_test.dart +++ b/test/reactive_builder_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -6,9 +7,9 @@ import 'package:rx_widgets/src/reactive_builder.dart'; void main() { testWidgets("When stream receive a data ", (tester) async { - var _controller = StreamController(); + final _controller = StreamController(); - var widget = ReactiveBuilder( + final widget = ReactiveBuilder( stream: _controller.stream, builder: (_, data) => MaterialApp(home: Text(data)), ); @@ -89,15 +90,16 @@ void main() { await _controller.close(); }); - testWidgets("When ReactiveBuilder receive a erro", (tester) async { - var _controller = StreamController(); + testWidgets("When ReactiveBuilder receive a error", (tester) async { + final _controller = StreamController(); - var widget = MaterialApp( - home: ReactiveBuilder( - stream: _controller.stream, - builder: (_, data) => Text(data), - errorBuilder: (_, error) => Text(error), - )); + final widget = MaterialApp( + home: ReactiveBuilder( + stream: _controller.stream, + builder: (_, data) => Text(data), + errorBuilder: (_, error) => Text(error.toString()), + ), + ); _controller.addError("Custom Error"); await tester.pumpWidget(widget); diff --git a/test/selector_test.dart b/test/selector_test.dart index eef67a6..7cc15c2 100644 --- a/test/selector_test.dart +++ b/test/selector_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:rx_widgets/src/widget_selector.dart'; @@ -8,11 +9,12 @@ void main() { var _controller = StreamController(); var widget = MaterialApp( - home: WidgetSelector( - buildEvents: _controller.stream, - onFalse: Text("FALSE"), - onTrue: Text("TRUE"), - )); + home: WidgetSelector( + buildEvents: _controller.stream, + onFalse: Text("FALSE"), + onTrue: Text("TRUE"), + ), + ); _controller.add(true); await tester.pumpWidget(widget); @@ -71,11 +73,12 @@ void main() { var _controller = StreamController(); var widget = MaterialApp( - home: WidgetBuilderSelector( - buildEvents: _controller.stream, - onTrue: (_) => Text("TRUE"), - onFalse: (_) => Text("FALSE"), - )); + home: WidgetBuilderSelector( + buildEvents: _controller.stream, + onTrue: (_) => Text("TRUE"), + onFalse: (_) => Text("FALSE"), + ), + ); _controller.add(false); await tester.pumpWidget(widget);