I try to localize a String in Flutter with the localization package. The problem is the location where my translation is needed. It is not related to the UI, rather it is somewhere deep in my model, where I don't have access to a BuildContext. Is there any other possibility to still make use of the translation function?
// I don't have a context variable here
MyLocalizations.of(context).trans("foo")
Yes there is. You don't need BuildContext to access strings. Here is my solution:
class Strings {
Strings._(Locale locale) : _localeName = locale.toString() {
current = this;
}
final String _localeName;
static Strings current;
static Future<Strings> load(Locale locale) async {
await initializeMessages(locale.toString());
final result = Strings._(locale);
return result;
}
static Strings of(BuildContext context) {
return Localizations.of<Strings>(context, Strings);
}
String get title {
return Intl.message(
'Hello World',
name: 'title',
desc: 'Title for the Demo application',
);
}
}
Future<Null> main() async {
final Locale myLocale = Locale(window.locale);
await Strings.load(myLocale);
runApp(MyApplication());
}
Now you can reference a string as follows:
final title = Strings.current.title;
I know this question is dated way back. But I came across this issue when implementing my application, and I dont see any "nice" way to handle it.
So here is my approach
class LanguageService {
static String defaultLanguage = 'en';
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Storefront',
'language': 'Language',
'googleLogin': 'Login with Google'
},
'vn': {
'title': 'Cửa hàng',
'language': 'Ngôn ngữ',
'googleLogin': 'Đăng Nhập với Google'
}
};
static set language(String lang) {
defaultLanguage = lang;
}
static String get title {
return _localizedValues[defaultLanguage]['title'];
}
static String get language {
return _localizedValues[defaultLanguage]['language'];
}
static String get googleLogin {
return _localizedValues[defaultLanguage]['googleLogin'];
}
}
Now you can reference a string as follows:
String title = LanguageService.title;
You can find the detailed tutorial here
AppLocalitzations needs the context.
You can create a class (e.g., Localization) to encapsulate AppLocalizations initialization and initialize it from the home widget using its context. After, can be used with a mixin:
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class Localization {
static AppLocalizations _loc;
AppLocalizations get loc => Localization._loc;
static void init(BuildContext context) => _loc = AppLocalizations.of(context);
}
In the home widget:
...
#override
Widget build(BuildContext context) {
Localization.init(context);
return Scaffold(
...
Access to loc in some class (it isn't necessary to be a Widget) using mixins:
class XXXWidget extends StatelessWidget with Localization {
...
Text(loc.xxxx)
...
}
class _XXXXWidgetState extends State<XXXWidget> with Localization {
...
Text(loc.xxxx)
...
}
class XXXXController with Localization {
...
cardNumberValidator = RequiredValidator(errorText: loc.commons_Required);
...
}
Null safety version:
class Localization {
static AppLocalizations? _l;
AppLocalizations get loc => Localization._l!;
static void init(BuildContext context) => _l = AppLocalizations.of(context)!;
}
No, there is no other way because it is stored using an InheritedWidget, which is a part of the build tree and thus can only be accessed with a reference to it (the BuildContext).
You will need to pass your context to somewhere deep in your model.
I am not sure if i did it right (from performance point of view) and maybe someone can comment on this but i have rx BehaviorSubject in my AppLocalization and fire event once new locales are loaded. I am listening to it in my main.dart and doing setState on receiving an event.
I checked performance tab but did not noticed any big changes in it once comparing my method vs accessing translations through context (inherited widget).
Related
For instance, in Javascript I can do something like:
class Foo {
x = 'baz';
bar() {
const someVar = 'x';
console.log(this[someVar]);
// Output: 'baz';
}
}
Hopefully that's relatively clear - it boils down to accessing a member variable by another variable's contents. How is this achieved in Dart?
This is not trivial in Dart. Dart doesn't have a syntax to access class properties with [].
There are a couple of approaches though:
Mirrors:
https://api.dartlang.org/stable/2.6.1/dart-mirrors/dart-mirrors-library.html
Basically you have access to everything and offers the biggest freedom. You can check what properties a class has, access them via names and so on. Big disadvantage is that the generated JS (if targeting web) will be huge. Flutter doesn't support it at all.
Reflectable
To deal with the large generated JS, you can use package:reflectable. Never tried it with Flutter. It's a bit more to set up and start using bit it works.
Dart only solution 1
You can overload [] operator on a class:
class Foo {
final _backing = <String, String>{
'foo': 'bar'
};
operator [](String val) {
return _backing[val];
}
}
void main() {
final inst = Foo();
print(inst['foo']);
}
Dart only solution 2
Just use a map :) Well sort of... If you are dealing with complex types and you want to add some extra functionality to your map, you can do something like this:
import 'dart:collection';
class StringMap extends Object with MapMixin<String, String> {
final _backing = <String, String>{};
#override
String operator [](Object key) {
return _backing[key];
}
#override
void operator []=(String key, String value) {
_backing[key] = value;
}
#override
void clear() {
_backing.clear();
}
#override
Iterable<String> get keys => _backing.keys;
#override
String remove(Object key) {
return _backing.remove(key);
}
}
I'm writing a code generator for Dart using the build_runner, but my builder is not being called for annotations at fields, although it does work for annotations at classes.
Is it possible to also call the generator for annotations at fields (or at any place for that matter)?
For example, the builder is called for the following file:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
#MyAnnotation()
class Fruit {
int number;
}
But not for this one:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
class Fruit {
#MyAnnotation()
int number;
}
Here's the definition of the annotation:
class MyAnnotation {
const MyAnnotation();
}
And this is how the generator is defined. For now, it just aborts whenever it's called, causing an error message to be printed.
library my_annotation_generator;
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:my_annotation/my_annotation.dart';
import 'package:source_gen/source_gen.dart';
Builder generateAnnotation(BuilderOptions options) =>
SharedPartBuilder([MyAnnotationGenerator()], 'my_annotation');
class MyAnnotationGenerator extends GeneratorForAnnotation<MyAnnotation> {
#override
generateForAnnotatedElement(Element element, ConstantReader annotation, _) {
throw CodeGenError('Generating code for annotation is not implemented yet.');
}
Here's the build.yaml configuration:
targets:
$default:
builders:
my_annotation_generator|my_annotation:
enabled: true
builders:
my_annotation:
target: ":my_annotation_generator"
import: "package:my_annotation/my_annotation.dart"
builder_factories: ["generateAnnotation"]
build_extensions: { ".dart": [".my_annotation.g.part"] }
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
At least from my experience, your file 'example.dart' would need at least one annotation above the class definition to be parsed by GeneratorForAnnotation.
example.dart:
import 'package:my_annotation/my_annotation.dart';
part 'example.g.dart';
#MyAnnotation()
class Fruit {
#MyFieldAnnotation()
int number;
}
To access annotations above class fields or class methods you could use a visitor to "visit" each child element and extract the source code information.
For example, to get information about the class fields you could override the method visitFieldElement and then access any annotations using the getter: element.metadata.
builder.dart:
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:source_gen/source_gen.dart';
import 'package:my_annotation/my_annotation.dart';
class MyAnnotationGenerator extends
GeneratorForAnnotation<MyAnnotation> {
#override
FutureOr<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,){
return _generateSource(element);
}
String _generateSource(Element element) {
var visitor = ModelVisitor();
element.visitChildren(visitor);
return '''
// ${visitor.className}
// ${visitor.fields}
// ${visitor.metaData}
''';
}
}
class ModelVisitor extends SimpleElementVisitor {
DartType className;
Map<String, DartType> fields = {};
Map<String, dynamic> metaData = {};
#override
visitConstructorElement(ConstructorElement element) {
className = element.type.returnType;
}
#override
visitFieldElement(FieldElement element) {
fields[element.name] = element.type;
metaData[element.name] = element.metadata;
}
}
Note: In this example, _generateSource returns a commented statement. Without comments you would need to return well-formed dart source code, otherwise, the builder will terminate with an error.
For more information see:
Source Generation and Writing Your Own Package (The Boring Flutter Development Show, Ep. 22) https://www.youtube.com/watch?v=mYDFOdl-aWM&t=459s
The built-in GeneratorForAnnotation uses the LibraryElement's annotatedWith(...) method, which only checks for top-level annotations.
To also detect annotations on fields, you'll need to write something custom.
Here's the Generator I wrote for my project:
abstract class GeneratorForAnnotatedField<AnnotationType> extends Generator {
/// Returns the annotation of type [AnnotationType] of the given [element],
/// or [null] if it doesn't have any.
DartObject getAnnotation(Element element) {
final annotations =
TypeChecker.fromRuntime(AnnotationType).annotationsOf(element);
if (annotations.isEmpty) {
return null;
}
if (annotations.length > 1) {
throw Exception(
"You tried to add multiple #$AnnotationType() annotations to the "
"same element (${element.name}), but that's not possible.");
}
return annotations.single;
}
#override
String generate(LibraryReader library, BuildStep buildStep) {
final values = <String>{};
for (final element in library.allElements) {
if (element is ClassElement && !element.isEnum) {
for (final field in element.fields) {
final annotation = getAnnotation(field);
if (annotation != null) {
values.add(generateForAnnotatedField(
field,
ConstantReader(annotation),
));
}
}
}
}
return values.join('\n\n');
}
String generateForAnnotatedField(
FieldElement field, ConstantReader annotation);
}
I had a very similar issue trying to target specific methods within my annotated classes. Inspired by your answers I slightly modified the class annotation model_visitor to check the method annotation before selecting elements.
class ClassAnnotationModelVisitor extends SimpleElementVisitor<dynamic> {
String className;
Map<String, String> methods = <String, String>{};
Map<String, String> parameters = <String, String>{};
#override
dynamic visitConstructorElement(ConstructorElement element) {
final elementReturnType = element.type.returnType.toString();
className = elementReturnType.replaceFirst('*', '');
}
#override
dynamic visitMethodElement(MethodElement element) {
if (methodHasAnnotation(MethodAnnotation, element)) {
final functionReturnType = element.type.returnType.toString();
methods[element.name] = functionReturnType.replaceFirst('*', '');
parameters[element.name] = element.parameters.map((e) => e.name).join(' ,');
}
}
bool methodHasAnnotation(Type annotationType, MethodElement element) {
final annotations = TypeChecker.fromRuntime(annotationType).annotationsOf(element);
return !annotations.isEmpty;
}
}
Then, I can use the basic GeneratorForAnnotation class and generate for class and methodsArray.
I did the following test, but it doesn't work:
//main.dart
class Test
{
static const a = 10;
final b = 20;
final c = a+1;
}
//part.dart
part of 'main.dart';
class Test
{
final d = a +1; //<---undefined name 'a'
}
I would like to split the class in flutter tutorial into multiple files. For example: _buildSuggestions in a separate file, _buildRow in a separate file, etc.
update:
my solution:
before:
//main.dart
class RandomWordsState extends State<RandomWords> {
{
final _var1;
final _var2;
#override
Widget build(BuildContext context) {
...
body: _buildList(),
);
Widget _buildList() { ... }
Widget _buildRow() { ... }
}
after:
//main.dart
import 'buildlist.dart';
class RandomWordsState extends State<RandomWords> {
{
final var1;
final var2;
#override
Widget build(BuildContext context) {
...
body: buildList(this),
);
}
//buildlist.dart
import 'main.dart';
Widget buildList(RandomWordsState obj) {
... obj.var1 ...
}
I am faced with same problem. My variant based on extensions:
page.dart
part 'section.dart';
class _PageState extends State<Page> {
build(BuildContext context) {
// ...
_buildSection(context);
// ...
}
}
section.dart
part of 'page.dart';
extension Section on _PageState {
_buildSection(BuildContext context) {
// ...
}
}
Dart doesn't support partial classes. part and part of are to split a library into multiple files, not a class.
Private (identifiers starting with _) in Dart is per library which is usually a *.dart file.
main.dart
part 'part.dart';
class Test {
/// When someone tries to create an instance of this class
/// Create an instance of _Test instead
factory Test() = _Test;
/// private constructor that can only be accessed within the same library
Test._();
static const a = 10;
final b = 20;
final c = a+1;
}
part.dart
part of 'main.dart';
class _Test extends Test {
/// private constructor can only be called from within the same library
/// Call the private constructor of the super class
_Test() : super._();
/// static members of other classes need to be prefixed with
/// the class name, even when it is the super class
final d = Test.a +1; //<---undefined name 'a'
}
A similar pattern is used in many code-generation scenarios in Dart like in
https://pub.dartlang.org/packages/built_value
https://pub.dartlang.org/packages/built_redux
https://pub.dartlang.org/packages/json_serializable
and many others.
I just extend it with extension keyword like Swift.
// class_a.dart
class ClassA {}
// class_a+feature_a.dart
import 'class_a.dart';
extension ClassA_FeatureA on ClassA {
String separatedFeatureA() {
// do your job here
}
}
Please ignore the coding conventions, it's just a sample.
I'm currently test driving and I have already built a localized "Hello world" following the tutorial. However, when I tried to move my widget to a different file, I got a red screen of death with the error:
The following NoSuchMethodError was thrown building ToDoItem(dirty):
The method 'helloWorld' was called on null.
Receiver: null
Tried calling: helloWorld()
My To Do item class looks like this
todo_item.dart
import 'package:flutter/material.dart';
import 'package:to_do_app/localization.dart';
class ToDoItem extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Text(ToDoLocalizations.of(context).helloWorld());
}
}
Of course the problem is that my Localization class has not been initialized, however I don't know how to initialize it since the Text widget does not have a LocalizationDelegates parameter.
I'm aware that this could be fixed by injecting the String directly into my widget's constructor, but for the sake of it I want to know how to localize widgets.
EDIT: Here is my localization class
import 'dart:async';
import 'dart:developer';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:to_do_app/l10n/messages_all.dart';
class ToDoLocalizations {
ToDoLocalizations(Locale locale) : _localeName = locale.toString();
final String _localeName;
static Future<ToDoLocalizations> load(Locale locale) {
return initializeMessages(locale.toString()).then((Object _) {
return new ToDoLocalizations(locale);
});
}
static ToDoLocalizations of(BuildContext context) {
return Localizations.of<ToDoLocalizations>(context, ToDoLocalizations);
}
String helloWorld() {
return Intl.message(
'Hello, World!',
name: 'helloWorld',
desc: 'A friendly salutation',
locale: _localeName
);
}
}
class ToDoLocalizationsDelegate
extends LocalizationsDelegate<ToDoLocalizations> {
#override
bool isSupported(Locale locale) {
return ['en', 'nb'].contains(locale.languageCode);
}
#override
bool shouldReload(LocalizationsDelegate<ToDoLocalizations> old) {
return false;
}
#override
Future<ToDoLocalizations> load(Locale locale) {
return ToDoLocalizations.load(locale);
}
}
After closely following the aforementioned tutorial and the code outlined on this example repository without success, this answer pointed me in the right direction. All of my code was sound and the only missing part was to write import statements using relative paths instead of absolute paths and then the error disappeared.
I had added the MaterialApp widget in main.dart, further again in some of files i added MaterialApp by accident. So, adding the second MaterialApp widget overrode what I had previously defined.
I was wondering if is possible to create an instance of a generic type in Dart. In other languages like Java you could work around this using reflection, but I'm not sure if this is possible in Dart.
I have this class:
class GenericController <T extends RequestHandler> {
void processRequest() {
T t = new T(); // ERROR
}
}
I tried mezonis approach with the Activator and it works. But it is an expensive approach as it uses mirrors, which requires you to use "mirrorsUsed" if you don't want to have a 2-4MB js file.
This morning I had the idea to use a generic typedef as generator and thus get rid of reflection:
You define a method type like this: (Add params if necessary)
typedef S ItemCreator<S>();
or even better:
typedef ItemCreator<S> = S Function();
Then in the class that needs to create the new instances:
class PagedListData<T>{
...
ItemCreator<T> creator;
PagedListData(ItemCreator<T> this.creator) {
}
void performMagic() {
T item = creator();
...
}
}
Then you can instantiate the PagedList like this:
PagedListData<UserListItem> users
= new PagedListData<UserListItem>(()=> new UserListItem());
You don't lose the advantage of using generic because at declaration time you need to provide the target class anyway, so defining the creator method doesn't hurt.
You can use similar code:
import "dart:mirrors";
void main() {
var controller = new GenericController<Foo>();
controller.processRequest();
}
class GenericController<T extends RequestHandler> {
void processRequest() {
//T t = new T();
T t = Activator.createInstance(T);
t.tellAboutHimself();
}
}
class Foo extends RequestHandler {
void tellAboutHimself() {
print("Hello, I am 'Foo'");
}
}
abstract class RequestHandler {
void tellAboutHimself();
}
class Activator {
static createInstance(Type type, [Symbol constructor, List
arguments, Map<Symbol, dynamic> namedArguments]) {
if (type == null) {
throw new ArgumentError("type: $type");
}
if (constructor == null) {
constructor = const Symbol("");
}
if (arguments == null) {
arguments = const [];
}
var typeMirror = reflectType(type);
if (typeMirror is ClassMirror) {
return typeMirror.newInstance(constructor, arguments,
namedArguments).reflectee;
} else {
throw new ArgumentError("Cannot create the instance of the type '$type'.");
}
}
}
I don't know if this is still useful to anyone. But I have found an easy workaround. In the function you want to initialize the type T, pass an extra argument of type T Function(). This function should return an instance of T. Now whenever you want to create object of T, call the function.
class foo<T> {
void foo(T Function() creator) {
final t = creator();
// use t
}
}
P.S. inspired by Patrick's answer
2022 answer
Just came across this problem and found out that although instantiating using T() is still not possible, you can get the constructor of an object easier with SomeClass.new in dart>=2.15.
So what you could do is:
class MyClass<T> {
final T Function() creator;
MyClass(this.creator);
T getGenericInstance() {
return creator();
}
}
and when using it:
final myClass = MyClass<SomeOtherClass>(SomeOtherClass.new)
Nothing different but looks cleaner imo.
Here's my work around for this sad limitation
class RequestHandler {
static final _constructors = {
RequestHandler: () => RequestHandler(),
RequestHandler2: () => RequestHandler2(),
};
static RequestHandler create(Type type) {
return _constructors[type]();
}
}
class RequestHandler2 extends RequestHandler {}
class GenericController<T extends RequestHandler> {
void processRequest() {
//T t = new T(); // ERROR
T t = RequestHandler.create(T);
}
}
test() {
final controller = GenericController<RequestHandler2>();
controller.processRequest();
}
Sorry but as far as I know, a type parameter cannot be used to name a constructor in an instance creation expression in Dart.
Working with FLutter
typedef S ItemCreator<S>();
mixin SharedExtension<T> {
T getSPData(ItemCreator<T> creator) async {
return creator();
}
}
Abc a = sharedObj.getSPData(()=> Abc());
P.S. inspired by Patrick
simple like that.
import 'dart:mirrors';
void main(List<String> args) {
final a = A<B>();
final b1 = a.getInstance();
final b2 = a.getInstance();
print('${b1.value}|${b1.text}|${b1.hashCode}');
print('${b2.value}|${b2.text}|${b2.hashCode}');
}
class A<T extends B> {
static int count = 0;
T getInstance() {
return reflectClass(T).newInstance(
Symbol(''),
['Text ${++count}'],
{Symbol('value'): count},
).reflectee;
}
}
class B {
final int value;
final String text;
B(this.text, {required this.value});
}
Inspired by Patrick's answer, this is the factory I ended up with.
class ServiceFactory<T> {
static final Map<Type, dynamic> _cache = <String, dynamic>{};
static T getInstance<T>(T Function() creator) {
String typeName = T.toString();
return _cache.putIfAbsent(typeName, () => creator());
}
}
Then I would use it like this.
final authClient = ServiceFactory.getInstance<AuthenticationClient>(() => AuthenticationClient());
Warning: Erik made a very good point in the comment below that the same type name can exist in multiple packages and that will cause issues. As much as I dislike to force the user to pass in a string key (that way it's the consumer's responsibility to ensuring the uniqueness of the type name), that might be the only way.