How to configure Flutter Flavor for different API links - flutter

I have 3 different API links for Staging, Development and Production Stages. I tried to configure the files in this way to share links
//Types of flavors we have
import 'dart:io';
//Types of flavors
enum Flavor {
STAGING,
DEVELOPMENT,
PRODUCTION,
}
class Config {
//Floavor is the way to devide application
//configurations depending on stage we work
static Flavor? appFlavor;
// api url for requests depending on the flavor
// you can use it by typing Config.api_url
static String get api_url {
switch (appFlavor) {
case Flavor.PRODUCTION:
return 'https://api.Link1';
case Flavor.DEVELOPMENT:
return 'https://api.Link2';
case Flavor.STAGING:
return 'https://api.Link3';
default:
return 'https://api.Link2';
}
}
//getting information about platform
//you can use it by typing Config.platform
static String get platform => Platform.isAndroid ? 'ANDROID' : 'IOS';
}
Then I initialise the flavor in main.dart in the following way :
void main() {
//Initialising the flavor
Config.appFlavor = Flavor.PRODUCTION;
Bloc.observer = AppBlocObserver();
FlutterError.onError = (details) {
log(details.exceptionAsString(), stackTrace: details.stack);
};
runZonedGuarded(
() => runApp(const IChazy()),
(error, stackTrace) => log(error.toString(), stackTrace: stackTrace),
);
}
But then when I try to pass the data to Chopper package for baseURL it shows me mistake that I should use the const value:
import 'package:chopper/chopper.dart';
import 'package:ichazy/config/flavor_config.dart';
//part 'challenges_api_service.chopper.dart';
#ChopperApi(baseUrl: Config.api_url) //Error Appears here
abstract class ChallengeApiService {}
The Error:
Const variables must be initialized with a constant value.
Try changing the initializer to be a constant expression.
I know that I should use const value in order to cancel this error but in the same time I want to switch flavors and API links. Is there any proper way to somehow manage this two goals, maybe by changing Config class?
Thank you in advance.

The only way I found is to use custom create method in the chopper package
#ChopperApi(baseUrl: '/')
abstract class ChallengeApiService extends ChopperService {
#Get()
Future<Response> getChallenges(
#Body() Map<String, dynamic> body,
);
static ChallengeApiService create() {
final client = ChopperClient(
baseUrl: Config.api_url,
services: [_$ChallengeApiService()],
converter: JsonConverter());
return _$ChallengeApiService(client);
}
}

Related

How to disable admob for flutter web

i have a flutter app. It's working on web browsers and mobile devices. I have added ad mob for showing banner ads. But i cant run the project because of google_mobile_ads doesn't support web.
I'm starting google mobile ads if the current platform is a mobile device.
I have an export.dart like this:
export 'ad_mobile.dart' if (dart.library.html) 'ad_web.dart';
ad_mobile.dart:
import 'package:google_mobile_ads/google_mobile_ads.dart';
Future<dynamic> initAds() async {
await _initGoogleMobileAds();
}
Future<InitializationStatus> _initGoogleMobileAds() {
RequestConfiguration configuration = RequestConfiguration(
testDeviceIds: <String>[
testDeviceId,
],);
MobileAds.instance.updateRequestConfiguration(configuration);
return MobileAds.instance.initialize();
}
and ad_web.dart is:
import 'dart:developer';
Future initAds() async {
log('ADS DOES\'NT SUPPORTED FOR WEB PLATFORMS');
}
When i run the app on Chrome, app starts but stuck at white screen. And i get this error on debug console:
Error: MissingPluginException(No implementation found for method _init on channel plugins.flutter.io/google_mobile_ads)
There are ways to make it work with less code but if this way it is easier to maintain and extend your code.
You need to change how you import your files based on the platform.
First create an abstract class around Admob:
abstract class AdmobWrapper{
factory AdmobWrapper() => createAdmobWrapper();
Future<dynamic> init();
}
Second create the platform files:
Generic platform:
AdmobWrapper createAdmobWrapper() => throw UnimplementedError('createAdmobWrapper()');
IO (Mobile) platform:
AdmobWrapper createAdmobWrapper() => AdmobWrapperIO();
class AdmobWrapperIO() implements AdmobWrapper(){
factory AdmobWrapperIO() => _instance;
AdmobWrapperIO._internal();
static final AdmobWrapperIO_instance = AdmobWrapperIO._internal();
Future<dynamic> init(){
.... do init here
}
}
Web platform:
AdmobWrapper createAdmobWrapper() => AdmobWrapperWeb();
class AdmobWrapperWeb implements AdmobWrapper {
factory AdmobWrapperWeb() => _instance;
AdmobWrapperWeb._internal();
static final AdmobWrapperWeb _instance = AdmobWrapperWeb._internal();
Future<dynamic> init(){
// nothing here
}
}
Third add platform imports to your abstract class:
import 'package:genericPlatform.dart' // <-- this 2 slash is important
if(dart.library.io) 'package:yourRealAdmobPackage'
if(dart.library.html) 'package:yourWebPackage'
abstract class AdmobWrapper(){ /// rest of the code we made in first step
You can call AdmobWrapper().init() now.

How to mock function in flutter test

How can I mock a function in flutter and verify it has been called n times?
Ive tried implementing Mock from mockito but it only throws errors:
class MockFunction extends Mock {
call() {}
}
test("onListen is called once when first listener is registered", () {
final onListen = MockFunction();
// Throws: Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
when(onListen()).thenReturn(null);
bloc = EntityListBloc(onListen: onListen);
// If line with when call is removed this throws:
// Used on a non-mockito object
verify(onListen()).called(1);
});
});
As a workaround I am just manually tracking the calls:
test("...", () {
int calls = 0;
bloc = EntityListBloc(onListen: () => calls++);
// ...
expect(calls, equals(1));
});
So is there a way I can create simple mock functions for flutter tests?
What you could do is this:
class Functions {
void onListen() {}
}
class MockFunctions extends Mock implements Functions {}
void main() {
test("onListen is called once when first listener is registered", () {
final functions = MockFunctions();
when(functions.onListen()).thenReturn(null);
final bloc = EntityListBloc(onListen: functions.onListen);
verify(functions.onListen()).called(1);
});
}
The accepted answer is correct, but it doesn't represent a real-life scenario where you will probably want to substitute a top-level function with a mock or a fake. This article explains how to include top-level functions in your dependency injection composition so that you can substitute those functions with mocks.
You can compose dependency injection like this and point to top-level functions such as launchUrl with ioc_container.
IocContainerBuilder compose() => IocContainerBuilder(
allowOverrides: true,
)
..addSingletonService<LaunchUrl>(
(url, {mode, webOnlyWindowName, webViewConfiguration}) async =>
launchUrl(
url,
mode: mode ?? LaunchMode.platformDefault,
webViewConfiguration:
webViewConfiguration ?? const WebViewConfiguration(),
webOnlyWindowName: webOnlyWindowName,
),
)
..add((container) => MyApp(launchUrl: container<LaunchUrl>()));
Then, you can use the technique mentioned in the answer here to mock with Mocktail.
import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
class LaunchMock extends Mock {
Future<bool> call(
Uri url, {
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
});
}
void main() {
testWidgets('Test Url Launch', (tester) async {
//These allow default values
registerFallbackValue(LaunchMode.platformDefault);
registerFallbackValue(const WebViewConfiguration());
//Create the mock
final mock = LaunchMock();
when(() => mock(
flutterDevUri,
mode: any(named: 'mode'),
webViewConfiguration: any(named: 'webViewConfiguration'),
webOnlyWindowName: any(named: 'webOnlyWindowName'),
)).thenAnswer((_) async => true);
final builder = compose()
//Replace the launch function with a mock
..addSingletonService<LaunchUrl>(mock);
await tester.pumpWidget(
builder.toContainer()<MyApp>(),
);
//Tap the icon
await tester.tap(
find.byIcon(Icons.favorite),
);
await tester.pumpAndSettle();
verify(() => mock(flutterDevUri)).called(1);
});
}

Use a specific instance of a class inside an isolate

I am using an isolate through the compute() method to fetch, parse and sort datas from an API (around 10k entries).
My method getAllCards() is defined inside a class YgoProRepositoryImpl which has an instance of my remote datasource class YgoProRemoteDataSource it is in this class that the method to call my API is defined (it is a simple GET request).
Code Sample
ygopro_repository_impl.dart
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
// ...
YgoProRepositoryImpl({
required this.remoteDataSource,
// ...
});
// ...
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
// ...
}
service_locator.dart
import 'package:get_it/get_it.dart';
import 'data/api/api.dart';
import 'data/datasources/remote/ygopro_remote_data_source.dart';
import 'data/repository/ygopro_repository_impl.dart';
import 'domain/repository/ygopro_repository.dart';
final sl = GetIt.instance;
void setupLocator() {
// ...
_configDomain();
_configData();
// ...
_configExternal();
}
void _configDomain() {
//! Domain
// ...
// Repository
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
// ...
),
);
}
void _configData() {
//! Data
// Data sources
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(sl<RemoteClient>()),
);
// ...
}
void _configExternal() {
//! External
sl.registerLazySingleton<RemoteClient>(() => DioClient());
// ...
}
The code is working properly but getAllCards() is not testable as I cannot inject a mocked class of YgoProRemoteDataSource inside my isolate because it will always get a reference from my service locator.
How can I do to not rely on my service locator to inject YgoProRemoteDataSource inside my isolate and make getAllCards() testable ?
Did a more serious attempt, please see the repo: https://github.com/maxim-saplin/compute_sl_test_sample
Essentially with the current state of affairs with Flutter/Dart you can't pass neither closures nor classes containing closures across isolates boundaries (yet that might change when newer features in Dart land Flutter https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528). That means there's no way you can pass service locator (which contains closures) or trick the isolate to instantiate a test version of locator via closure IF you don't want any test code to be part of the release build. Yet you can easily pass data source instance to isolate to be used at its entry point as a param.
Beside, I don't think asking isolate to rebuild the entire service locator makes sense. The whole idea behind compute() is to create a short leaving isolate, run the computation, return the result and terminate the isolate. Initialising the locator is an overhead which is better to be avoided. Besides it seems the whole concept of compute() is being as isolated from the rest of the app as possible.
You can clone the repo and run the tests. Few words about the sample:
Based on Flutter counter starter app
lib/classes.dart recreates the code snippet you provided
test/widget_test.dart verifies that YgoProRepositoryImpl is working fine with isolate running fake version of data source
YgoProRemoteDataSourceImpl mimics real implementation and is located at classes.dart and YgoProRemoteDataSourceFake mimics test version
Running isolates under flutter_test requires wrapping test body in tester.runAsync() in order to have real time async execution (rather than fake async used by default by tests and relying on pumping to progress test time). Running tests in this mode can be slow (there's actual 0.5 second wait), structuring the tests in a way when compute() is not used or tested not in many tests is reasonable
classes.dart
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
final sl = GetIt.instance;
class YgoCard {
YgoCard(this.name);
final String name;
}
abstract class YgoProRemoteDataSource {
Future<List<YgoCard>> getCardInfo();
}
class YgoProRemoteDataSourceImpl extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(5, (index) => YgoCard("Impl $index")));
}
}
abstract class YgoProRepository {
Future<List<YgoCard>> getAllCards();
}
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
YgoProRepositoryImpl({
required this.remoteDataSource,
});
static Future<List<YgoCard>> _fetchCards(
YgoProRemoteDataSource dataSource) async {
final cards = await dataSource.getCardInfo();
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, remoteDataSource);
return cards;
}
}
void setupLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(),
);
}
widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:test_sample/classes.dart';
import 'package:test_sample/main.dart';
void main() {
setUpAll(() async {
setupFakeLocator();
});
testWidgets('Test mocked data source', (WidgetTester tester) async {
// Wrapping with runAync() is required to have real async in place
await tester.runAsync(() async {
await tester.pumpWidget(const MyApp());
// Let the isolate spawned by compute() complete, Debug run might require longer wait
await Future.delayed(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.text('Fake 9'), findsOneWidget);
});
});
}
class YgoProRemoteDataSourceFake extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(10, (index) => YgoCard("Fake $index")));
}
}
void setupFakeLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceFake(),
);
}
Do you really need to test the getCards() function?
What are you really testing there? That compute works, sure hope the Dart SDK team has a test for this.
That leaves _fetchCards(), and setupLocator() doesn't need to be tested either, it is precondition for your test-logic. You want to change the setup for the test anyways.
So what you actually want to test is the fetching & sorting. Restructure this into a testable static function and setup your locator beforehand. Put a #visibleForTesting annotation on it.
And on a side-note, depending on how much you bind in your service locator, this could be huge overhead for just using the one repository afterwards.
Example:
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
return reallyFetchCards();
}
#visibleForTesting
static Future<List<YgoCard>> reallyFetchCards() async {
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Test:
// Setup SL and datasource
...
final cards = await YgoProRepositoryImpl.reallyFetchCrads();
// Expect stuff
As I understand you have two options, either inject the dependencies needed for static Future<List<YgoCard>> _fetchCards(_) async via parameters, or mock the object in the locator itself. I would go for the fist option, and have something like :
static Future<List<YgoCard>> _fetchCards(_,YgoProRemoteDataSource remote) async {
// No need to set up locator as you passed the needed dependencies
// setupLocator();
final cards = await remote
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Edit
just updated the answer as its easier to edit this here than in the comments...
Hmm, the only workaround that I can think of is to pass the setupLocator() function as an argument to the class YgoProRepositoryImpl :
final Function setupLocator;
YgoProRepositoryImpl({
required this.remoteDataSource,
required this.setupLocator;
// ...
});
This way you could pass a mock that sets up your mock classes or the real setupLocator of your service_locator.dart. This might not be to elegant. But it should make it testable as now you can mock the setup and its not hardcoded in the function

Arguments of a constant creation must be constant expressions on Flutter

I used Flavor and define two main classes. And also I used Chopper. How to set baseUrl based one environment?
import 'package:meta/meta.dart';
enum BuildFlavor { production, development }
BuildEnvironment get env => _env;
BuildEnvironment _env;
class BuildEnvironment {
final String ssoBaseUrl;
final BuildFlavor flavor;
BuildEnvironment._init(
{this.ssoBaseUrl,
this.flavor});
static void init(
{#required flavor,
#required ssoBaseUrl}) =>
_env ??= BuildEnvironment._init(
flavor: flavor,
ssoBaseUrl: ssoBaseUrl);
}
main.dev.dart
void main() {
BuildEnvironment.init(
flavor: BuildFlavor.development,
ssoBaseUrl: 'http://15.88.219.20');
assert(env != null);
runApp(MultiProvider(
providers: globalProviders,
child: MyApp(),
));
}
Chopper
#ChopperApi(baseUrl:'${env.ssoBaseUrl}')
abstract class Services extends ChopperService {
static Services create([ChopperClient client]) => _$Services(client);
#Post(path: '/sso-dev/registration')
Future<Response> registerPost(
Error Message
Annotations have to be provided with const values only. Since env is a getter property, it is not const. I'm afraid you won't be able to specify a baseUrl this way.

Flutter - Load assets for tests

Not sure if it's a limitation or something, but below code does not load anything. I have some data driven behaviour that I'd like to test isolated.
class Loader
import 'dart:async';
import 'package:flutter/services.dart' show rootBundle;
class Loader {
Future<String> load() async{
return await rootBundle.loadString('assets/json/sketch.json');
}
}
The test
testWidgets('Should parse load sketch.json', (WidgetTester tester) async {
var loaderFuture = new Loader();
Future<String> resultFuture = loaderFuture.load();
resultFuture.then((value) => print(value))
.catchError((error) => print(error));
while(true){};
});
Future does not return neither success nor error and hangs forever. I know
the while(true) locking up the test, but for now I just wanted to see sketch.json printed
Asset location
To use rootBundle in your tests you need this at the beginning of your test programs:
import 'package:flutter_test/flutter_test.dart';
...
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
See the documentation of DefaultAssetBundle it describes using it and a AssetBundle to provide your own assets.
Create a class that wraps the rootBundle:
#injectable
class AssetsManager {
Future<String> loadString(String path) {
return rootBundle.loadString(path);
}
}
Then inject it to your class and in your test override its dependency:
getIt.unregister<AssetsManager>();
getIt.registerSingleton<AssetsManager>(AssetsManagerMock());
Based on your test scenario, Configure what will be returned when calling loadString by using Mocktail's when method.
I'm using get_it for DI. Hope it's clear enough.