I use riverpod for state management in flutter development.
I'm now trying to create a common component that needs state management.
I use riverpod to make a common component with like the following code.
class InputTextareaModel with ChangeNotifier {
String text;
String error;
String Function(String text) _validator;
void Function(String text) _onChanged;
bool _autoValidation;
set validator(String Function(String text) validator) {
this._validator = validator ?? (String text) => null;
}
set onChanged(void Function(String text) onChanged) {
this._onChanged = onChanged ?? (String text) {};
}
set autoValidation(bool autoValidation) {
this._autoValidation = autoValidation ?? true;
}
InputTextareaModel() {
this.text = '';
this.error = null;
}
onChangedText(String value) {
this.text = value;
this._onChanged(value);
if (this._autoValidation) {
this.onValidation();
}
notifyListeners();
}
onValidation() {
this.error = this._validator(this.text);
notifyListeners();
}
bool isError() {
return this.error != null ? true : false;
}
}
class InputTextarea extends HookWidget {
final String label;
final int minLines;
final int maxLines;
final String placeholder;
final provider;
final String Function(String text) validator;
final void Function(String text) onChanged;
final bool autoValidation;
InputTextarea(
{Key key,
this.label,
this.placeholder,
provider,
this.validator,
this.onChanged,
this.autoValidation,
this.minLines = 3,
this.maxLines = 5})
: this.provider =
provider ?? ChangeNotifierProvider((_) => InputTextareaModel()),
super(key: key);
#override
Widget build(BuildContext context) {
final inputTextareaModel = useProvider<InputTextareaModel>(provider);
inputTextareaModel.validator = this.validator;
inputTextareaModel.onChanged = this.onChanged;
inputTextareaModel.autoValidation = this.autoValidation;
return TextField(
decoration: InputDecoration(
alignLabelWithHint: true,
labelText: this.label,
hintText: this.placeholder,
errorText: inputTextareaModel.error,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.multiline,
minLines: this.minLines,
maxLines: this.maxLines,
onChanged: (value) {
inputTextareaModel.onChangedText(value);
},
);
}
}
And I'm calling this common component with code like this:
final messageProvider =
ChangeNotifierProvider.autoDispose((_) => InputTextareaModel());
InputTextarea(
minLines: 5,
label: "message",
placeholder: "test",
provider: messageProvider, // We are injecting the provider at here.
validator: (value) {
if (value.replaceAll("\n", "").isEmpty) {
return 'required message!';
}
return null;
},
),
I can create a common component this way too, but I'm not quite happy with it.
I feel that there is a better way to create common components using riverpod.
Or is it better to create it using statefulWidget etc. without using riverpod?
If you know any good way, please let me know!
Thank you.
Related
I want to hide completely the password field, I set obscure text true but it shows the characters as I type them how hide completely the characters?
If you want completely hide it, you must use controller and implement like this:
class TextFieldPassWord extends StatefulWidget {
#override
_TextFieldPassWordState createState() => _TextFieldPassWordState();
}
class _TextFieldPassWordState extends State<TextFieldPassWord> {
String _valueShow = "";
String _value = "";
#override
Widget build(BuildContext context) {
return Scaffold(
body: TextField(
controller: TextEditingController.fromValue(
TextEditingValue(
text: _valueShow,
selection: TextSelection.collapsed(offset: _valueShow.length),
),
),
onChanged: (String str) {
String value = "";
if (str.length > _value.length) {
value += str.substring(_value.length, str.length);
}
if (str.length < _value.length) {
value = _value.substring(1, str.length);
}
String valueToShow = "*" * str.length;
setState(() {
_valueShow = valueToShow;
_value = value;
});
},
),
);
}
}
I am creating an App using riverpod, hooks and freezed. My aim is to remove the entire logic out of the widget tree. This article by #ttlg inspired me. In my app, I am trying to implement the following pattern. I am pretty new to riverpod so am i doing something wrong? You have my thanks.
what I am trying to achieve - image
my PasswordInputWidget
Widget build(BuildContext context, WidgetRef ref) {
final _authState = ref.watch(loginScreenStateProvider);
final _authController = ref.watch(authControllerProvider);
final _textController = useTextEditingController();
bool _isPasswordObscured = _authState.isPasswordObscured;
return TextField(
controller: _textController,
obscureText: _isPasswordObscured,
decoration: InputDecoration(
label: Text(AppLocalizations.of(context)!.password),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
suffixIcon: IconButton(
onPressed: () => _authController.toggleObscurePassword(),
icon: _isPasswordObscured
? const Icon(Icons.visibility_off_rounded)
: const Icon(Icons.visibility_rounded))),
onChanged: (value) =>
_authController.inputPassword(textController: _textController));
}
the logic is kept inside a file named auth_controller.dart as shown below
the controller updates the auth_state using user inputs.
The controller also handles the data coming from the repository which in turn updates the state.
abstract class AuthController {
void inputUserName({required TextEditingController textController});
void inputPassword({required TextEditingController textController});
void toggleObscurePassword();
bool checkUserNameisValid(String userName);
void login();
}
class AuthControllerImpl implements AuthController {
final Reader _read;
AuthControllerImpl(this._read);
#override
void inputUserName({required TextEditingController textController}) {
final state = _read(loginScreenStateProvider);
bool isValidUserName = checkUserNameisValid(textController.text);
_read(loginScreenStateProvider.notifier).setLoginScreenState(state.copyWith(
userName: textController.text, isValidUserName: isValidUserName));
}
#override
void toggleObscurePassword() {
final state = _read(loginScreenStateProvider);
_read(loginScreenStateProvider.notifier).setLoginScreenState(
state.copyWith(isPasswordObscured: !state.isPasswordObscured));
}
#override
void inputPassword({required TextEditingController textController}) {
final state = _read(loginScreenStateProvider);
_read(loginScreenStateProvider.notifier)
.setLoginScreenState(state.copyWith(password: textController.text));
}
#override
bool checkUserNameisValid(String userName) {
return TextUtils.isValidInput(
text: userName, exp: AppConstants.EMAIL_VALIDATOR);
}
#override
void login() {
final state = _read(loginScreenStateProvider);
final repository = _read(authRepositoryProvider);
if (state.userName.isNotEmpty &&
state.isValidUserName &&
state.password.isNotEmpty) {
repository.login(userName: state.userName, password: state.password);
}
}
}
final authControllerProvider = StateProvider.autoDispose<AuthControllerImpl>(
(ref) => AuthControllerImpl(ref.read));
The state is kept in a separate file auth_state.dart and is exposed using a getter and setter.
class LoginScreenState extends StateNotifier<LoginScreenModel> {
LoginScreenState() : super(const LoginScreenModel());
LoginScreenModel getLoginScreenState() {
return state;
}
void setLoginScreenState(LoginScreenModel newloginScreenState) {
state = newloginScreenState;
}
}
final loginScreenStateProvider =
StateNotifierProvider.autoDispose<LoginScreenState, LoginScreenModel>(
(ref) => LoginScreenState());
The model of the state is created using freezed as shown below
import 'package:freezed_annotation/freezed_annotation.dart';
part 'auth_model.freezed.dart';
#freezed
class LoginScreenModel with _$LoginScreenModel {
const factory LoginScreenModel(
{#Default('') String userName,
#Default('') String password,
#Default(true) bool isValidUserName,
#Default(true) bool isPasswordObscured,
#Default(false) bool showInvalidUserNameError}) = _LoginScreenModel;
}
API calls are handled in the repository as shown below.
//auth repository should be used to write data or fetch data from an api or local storage;
import 'package:dio/dio.dart';
import 'package:glowing/utils/api_utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
abstract class AuthRepository {
Future<void> login({required String userName, required String password});
Future<void> forgotPassword();
}
class AuthRepositoryImpl implements AuthRepository {
#override
Future<void> forgotPassword() {
// TODO: implement forgotPassword
throw UnimplementedError();
}
#override
Future<void> login(
{required String userName, required String password}) async {
const String endpoint = "https://example.com/v1/login";
Map<dynamic, dynamic> params = {'email': userName, 'password': password};
Response response = await APIUtils.post(endpoint, params);
print(response);
if (response.statusCode == 200) {
//just printing response now
print(response.data);
}
}
}
final authRepositoryProvider =
StateProvider.autoDispose<AuthRepository>((ref) => AuthRepositoryImpl());
Currently working on a way to translate romaji to hiragana directly in my TextField, I'v managed to get something working:
class RomajiTextInput extends StatefulWidget{
final bool mustConvertToKana;
const RomajiTextInput({Key key, this.mustConvertToKana}) : super(key: key);
#override
State<StatefulWidget> createState() => _RomajiTextInputState();
}
class _RomajiTextInputState extends State<RomajiTextInput> {
TextEditingController _titleEditingController;
TextEditingController _hiddenTitleEditingController;
String previousValue = "";
#override
void initState() {
super.initState();
_titleEditingController = new TextEditingController();
_hiddenTitleEditingController = new TextEditingController();
}
#override
void dispose() {
super.dispose();
_titleEditingController.clear();
_hiddenTitleEditingController.clear();
}
void onConversionChanged(String text){
if (widget.mustConvertToKana){
_hiddenTitleEditingController.text = getRomConversion(text, onlyRomaji: false);
String japanese = getJapaneseTranslation(_hiddenTitleEditingController.text, hasSpace: true);
int cursor = getCursorPosition(previousValue, japanese);
setState(() {
_titleEditingController.text = japanese;
_titleEditingController.selection = TextSelection.fromPosition(TextPosition(offset: japanese.length));
previousValue = japanese;
});
}
}
#override
Widget build(BuildContext context) {
return TextField(
controller: _titleEditingController,
onChanged: (text){
onConversionChanged(text);
},
decoration: InputDecoration(
labelText: 'Question',
labelStyle: TextStyle(fontSize: 20),
hintText:
'Enter a question / a word to remember'),
);
}
}
However, I would like to create a widget using these properties but also which can override all the different fields of a TextField such as "decoration", add some logic to the "onChanged" method or even add other functions from the TextField Widget such as "onEditingComplete".
In fact, I would like to be able to do something like this:
RomajiTextInput(
controller: myChildController,
onChanged: (text){print('text');} //And still convert to hiragana thanks to "onConversionChanged"
decoration: InputDecoration(...) //All options that have not been mentioned are kept otherwise just added
)
I bet there is a way to do so but extending the TextField Widget didn't get me anywhere...
Thank in advance :)
Consider bellow image, I want to dynamically change the text color of part of the text based on the user input text (not the whole text) in a text field. How can i do that in flutter?
For this example we actually don't need a full blown rich-text editor.
I had a similar goal in my app to highlight tags (#flutter) or date references (next week, on Friday, etc) and I was able to implement this by extending built-in EditableText widget and posted my example as a Gist here: https://gist.github.com/pulyaevskiy/d7af7217c2e71f31dfb78699f91dfbb5
Below is full implementation of this widget which I called AnnotatedEditableText.
There is new property annotations which describes ranges of text that need to be highlighted and their style.
import 'package:flutter/widgets.dart';
class Annotation extends Comparable<Annotation> {
Annotation({#required this.range, this.style});
final TextRange range;
final TextStyle style;
#override
int compareTo(Annotation other) {
return range.start.compareTo(other.range.start);
}
#override
String toString() {
return 'Annotation(range:$range, style:$style)';
}
}
class AnnotatedEditableText extends EditableText {
AnnotatedEditableText({
Key key,
FocusNode focusNode,
TextEditingController controller,
TextStyle style,
ValueChanged<String> onChanged,
ValueChanged<String> onSubmitted,
Color cursorColor,
Color selectionColor,
TextSelectionControls selectionControls,
this.annotations,
}) : super(
key: key,
focusNode: focusNode,
controller: controller,
cursorColor: cursorColor,
style: style,
keyboardType: TextInputType.text,
autocorrect: true,
autofocus: true,
selectionColor: selectionColor,
selectionControls: selectionControls,
onChanged: onChanged,
onSubmitted: onSubmitted,
);
final List<Annotation> annotations;
#override
AnnotatedEditableTextState createState() => new AnnotatedEditableTextState();
}
class AnnotatedEditableTextState extends EditableTextState {
#override
AnnotatedEditableText get widget => super.widget;
List<Annotation> getRanges() {
var source = widget.annotations;
source.sort();
var result = new List<Annotation>();
Annotation prev;
for (var item in source) {
if (prev == null) {
// First item, check if we need one before it.
if (item.range.start > 0) {
result.add(new Annotation(
range: TextRange(start: 0, end: item.range.start),
));
}
result.add(item);
prev = item;
continue;
} else {
// Consequent item, check if there is a gap between.
if (prev.range.end > item.range.start) {
// Invalid ranges
throw new StateError(
'Invalid (intersecting) ranges for annotated field');
} else if (prev.range.end < item.range.start) {
result.add(Annotation(
range: TextRange(start: prev.range.end, end: item.range.start),
));
}
// Also add current annotation
result.add(item);
prev = item;
}
}
// Also check for trailing range
final String text = textEditingValue.text;
if (result.last.range.end < text.length) {
result.add(Annotation(
range: TextRange(start: result.last.range.end, end: text.length),
));
}
return result;
}
#override
TextSpan buildTextSpan() {
final String text = textEditingValue.text;
if (widget.annotations != null) {
var items = getRanges();
var children = <TextSpan>[];
for (var item in items) {
children.add(
TextSpan(style: item.style, text: item.range.textInside(text)),
);
}
return new TextSpan(style: widget.style, children: children);
}
return new TextSpan(style: widget.style, text: text);
}
}
Rich text controller works fine!
See more on https://pub.dev/packages/rich_text_controller
First you choose your RegExp
RichTextController _controller;
Map<RegExp, TextStyle> patternUser = {
RegExp(r"\B#[a-zA-Z0-9]+\b"):
TextStyle(color: Colors.amber, fontWeight: FontWeight.bold)
};
on initState()
_controller = RichTextController(
patternMap: patternUser,
);
Add controller on your TextFormField
TextFormField(
controller: _controller,
style: TextStyle(color: Colors.white),
)
I'm implementing a custom text field and I would like to style certain keywords (namely hashtags) differently than the rest of the text as the user type them in.
Kind of like this:
Is there a way to do that in Flutter ?
This question is very similar to How to change color of particular text in a text field dynamically?
I answered it there in: https://stackoverflow.com/a/57846261/5280562
In short: you can extend EditableText widget including its EditableTextState class and override buildTextSpan method.
Below is a working example called AnnotatedEditableText that I use in my app.
You need to supply a list of Annotation objects which describe which ranges of text need to be highlighted and what style to use.
import 'package:flutter/widgets.dart';
class Annotation extends Comparable<Annotation> {
Annotation({#required this.range, this.style});
final TextRange range;
final TextStyle style;
#override
int compareTo(Annotation other) {
return range.start.compareTo(other.range.start);
}
#override
String toString() {
return 'Annotation(range:$range, style:$style)';
}
}
class AnnotatedEditableText extends EditableText {
AnnotatedEditableText({
Key key,
FocusNode focusNode,
TextEditingController controller,
TextStyle style,
ValueChanged<String> onChanged,
ValueChanged<String> onSubmitted,
Color cursorColor,
Color selectionColor,
TextSelectionControls selectionControls,
this.annotations,
}) : super(
key: key,
focusNode: focusNode,
controller: controller,
cursorColor: cursorColor,
style: style,
keyboardType: TextInputType.text,
autocorrect: true,
autofocus: true,
selectionColor: selectionColor,
selectionControls: selectionControls,
onChanged: onChanged,
onSubmitted: onSubmitted,
);
final List<Annotation> annotations;
#override
AnnotatedEditableTextState createState() => new AnnotatedEditableTextState();
}
class AnnotatedEditableTextState extends EditableTextState {
#override
AnnotatedEditableText get widget => super.widget;
List<Annotation> getRanges() {
var source = widget.annotations;
source.sort();
var result = new List<Annotation>();
Annotation prev;
for (var item in source) {
if (prev == null) {
// First item, check if we need one before it.
if (item.range.start > 0) {
result.add(new Annotation(
range: TextRange(start: 0, end: item.range.start),
));
}
result.add(item);
prev = item;
continue;
} else {
// Consequent item, check if there is a gap between.
if (prev.range.end > item.range.start) {
// Invalid ranges
throw new StateError(
'Invalid (intersecting) ranges for annotated field');
} else if (prev.range.end < item.range.start) {
result.add(Annotation(
range: TextRange(start: prev.range.end, end: item.range.start),
));
}
// Also add current annotation
result.add(item);
prev = item;
}
}
// Also check for trailing range
final String text = textEditingValue.text;
if (result.last.range.end < text.length) {
result.add(Annotation(
range: TextRange(start: result.last.range.end, end: text.length),
));
}
return result;
}
#override
TextSpan buildTextSpan() {
final String text = textEditingValue.text;
if (widget.annotations != null) {
var items = getRanges();
var children = <TextSpan>[];
for (var item in items) {
children.add(
TextSpan(style: item.style, text: item.range.textInside(text)),
);
}
return new TextSpan(style: widget.style, children: children);
}
return new TextSpan(style: widget.style, text: text);
}
}
It's also available in this Gist: https://gist.github.com/pulyaevskiy/d7af7217c2e71f31dfb78699f91dfbb5
I actually had the same problem and found the AnnotatedEditbleText which helped me a lot.
I published the helpful package to solve this kind of problem.
https://pub.dev/packages/hashtagable
The TextField does not provide that functionality.
https://pub.dartlang.org/packages/zefyr can do that though.
I think there are some more ''hard'' ways to do this
The first one:
Make a row widget, add a part of the String until the word you want to highlight, add the special word, style it and add the rest of your string.
Or, you could try RichText
Günter post it about the zefyr package, I didn't use it yet, but if suits you, I'll be glad that helped