flutter_markdown custom widget always on its own line - flutter

I'm using the flutter markdown package made by the flutter team here https://pub.dev/packages/flutter_markdown. I've created my own MarkdownElementBuilder based on their examples that inserts my own custom widget into the markdown and it looks like this:
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:outlit_app/constants/color_theme.dart';
import 'package:outlit_app/constants/dimension.dart';
import 'package:outlit_app/models/models.dart';
import 'package:markdown/markdown.dart' as md;
class DefinitionBuilder extends MarkdownElementBuilder {
final List<Definition> definitions;
DefinitionBuilder(this.definitions) : super();
#override
Widget visitElementAfter(md.Element element, TextStyle preferredStyle) {
final String textContent = element.textContent;
Definition definition = definitions.firstWhere(
(def) => textContent.toLowerCase().contains(def.word.toLowerCase()),
orElse: () =>
Definition(word: 'nothing found for $textContent', definition: ''),
);
return Tooltip(
margin: EdgeInsets.all(Dimensions.MARGIN_SIZE_EXTRA_LARGE),
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_DEFAULT),
decoration: BoxDecoration(
color: GetColor.gradientPurple,
borderRadius: BorderRadius.circular(8),
),
verticalOffset: -10,
triggerMode: TooltipTriggerMode.tap,
message: definition.definition.trim(),
child: Text(
textContent.trim(),
style: TextStyle(
color: GetColor.primaryColor,
fontSize: Dimensions.FONT_SIZE_OVER_LARGE,
),
),
);
}
}
class DefinitionSyntax extends md.InlineSyntax {
static final String AST_SYMBOL = 'def';
DefinitionSyntax() : super(_pattern);
static const String _pattern = r'{{(.*)}}';
#override
bool onMatch(md.InlineParser parser, Match match) {
parser.addNode(md.Element.text(AST_SYMBOL, match[1]));
return true;
}
}
It works well but the widget is always on it's own seperate line as opposed to being inline with the rest of the text. If I return a simple text widget I still get the same thing.
Any tips in the right direction would be great :)

I got it work although not perfect because the leading distribution is a little off with the text of the tooltip but the widget that gets embedded now looks like this:
return RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Container(
child: Tooltip(
margin: EdgeInsets.all(Dimensions.MARGIN_SIZE_EXTRA_LARGE),
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_DEFAULT),
decoration: BoxDecoration(
color: GetColor.gradientPurple,
borderRadius: BorderRadius.circular(8),
),
verticalOffset: -10,
triggerMode: TooltipTriggerMode.tap,
message: definition.definition.trim(),
child: Text(
textContent.trim(),
style: TextStyle(
color: GetColor.primaryColor,
fontSize: Dimensions.FONT_SIZE_OVER_LARGE,
leadingDistribution: TextLeadingDistribution.even,
height: 1,
),
),
),
))
],
),
);

Related

flutter/getx how to initialize nullable obejct observable

let's say I have a controller like this:
class ProfileController extends GetxController {
Rx<UserFacebookInfo?> facebookInfo = null.obs;
void facebookSync() async {
//
// logic to get user info from facebook
//
facebookInfo.value = UserFacebookInfo.fromFacebookApi(userData);
// facebookInfo = UserFacebookInfo.fromFacebookApi(userData).obs; <=== also tried this
update();
}
}
}
and in widget I have something like this:
Widget buildFacebook() => Padding(
padding: const EdgeInsets.only(top: 30.0, right: 20.0, left: 20.0, bottom: 10.0),
child: Obx(() => (_profileController.facebookInfo.value == null) ? Column(
children: [
IconButton(
icon : const Icon(Icons.facebook_outlined, size: 40.0),
onPressed: () => _profileController.facebookSync()
),
const Text(
'Facebook',
style: TextStyle(color: Colors.white),
)
],
) :
Column(
children: [
UserProfileAvatar(
avatarUrl: _profileController.facebookInfo.value!.facebookAvatar,
radius: 40,
),
Text(
_profileController.facebookInfo.value!.name,
style: const TextStyle(color: Colors.white),
)
],
)
));
and because initial value nullable, it's not working and not changing widget on a fly, but only if I update it from android studio. What is the correct way to initialize it??? I had similar case with nullable string observable so I was able to initialize it lie String string (null as String?).obs` Thanks for any advice
You can initialize nullable observables by using Rxn<Type>().
Therefore use this:
final facebookInfo= Rxn<UserFacebookInfo>();

Can a single TextField in flutter have variable line height?

I'm implementing a simple rich text editor that renders text with a text editing controller that recognises basic markdown syntax, I'll link some code down below.
Everything works fine, the only problem I'm having is when a text style requires a bigger line height, for instance an # h1 that should be rendered as a title and therefore require a bigger line height overlaps over the previous line, as you can see in the screenshot below.
I've not been able so far to make the line height in a TextView variable based on the style of the text that is being displayed, is such thing even achievable in a Flutter TextView?
Here's a snippet of my text editing controller and a screenshot detailing my problem.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class AddNotePage extends StatelessWidget {
final TextEditingController _controller = MarkdownTextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Note'),
),
body: GestureDetector(
onVerticalDragDown: (_) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: TextField(
style: defaultTextStyle,
controller: _controller,
decoration: InputDecoration(
hintText: "Insert your message",
border: UnderlineInputBorder(
borderSide: BorderSide.none,
),
),
scrollPadding: EdgeInsets.all(20.0),
keyboardType: TextInputType.multiline,
maxLines: null,
),
),
],
),
),
);
}
}
const Map<String, TextStyle> defaultMarkdownStyleMap = {
r'^# .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 50,
),
r'^## .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 40,
),
r'^### .*?$': TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
r'__(.*?)\__': TextStyle(fontStyle: FontStyle.italic, fontSize: 20),
r'~~(.*?)~~': TextStyle(decoration: TextDecoration.lineThrough, fontSize: 20),
r'\*\*(.*?)\*\*': TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
};
const TextStyle defaultTextStyle = TextStyle(fontSize: 20);
class MarkdownTextEditingController extends TextEditingController {
final Map<String, TextStyle> styleMap;
final Pattern pattern;
MarkdownTextEditingController({this.styleMap = defaultMarkdownStyleMap})
: pattern = RegExp(
styleMap.keys.map((key) {
return key;
}).join('|'),
multiLine: true);
#override
TextSpan buildTextSpan(
{required BuildContext context,
TextStyle? style,
required bool withComposing}) {
final List<InlineSpan> children = [];
text.splitMapJoin(
pattern,
onMatch: (Match match) {
TextStyle? markdownStyle = styleMap[styleMap.keys.firstWhere(
(e) {
return RegExp(e).hasMatch(match[0]!);
},
)];
children.add(TextSpan(
text: match[0],
style: style!.merge(markdownStyle),
));
return "";
},
onNonMatch: (String text) {
children
.add(TextSpan(text: text, style: style!.merge(defaultTextStyle)));
return "";
},
);
return TextSpan(style: style, children: children);
}
}
I've found a solution.
All I needed to do was to play around with the strutStyle property of the TextField.
As the documentation states:
The strut style used for the vertical layout.
StrutStyle is used to establish a predictable vertical layout. Since
fonts may vary depending on user input and due to font fallback,
StrutStyle.forceStrutHeight is enabled by default to lock all lines to
the height of the base TextStyle, provided by style. This ensures the
typed text fits within the allotted space.

Searchable SliverGrid Rendering Wrong Items

I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)

how to fix too many variables in flutter

I'm trying to create stacks of cards in my Flutter project. Each card contains different data/information and when I try visualize with a dummy data, I have to use a lot of variables which is pretty much repeating variable name for each card. Is there aways to make a reusable card component in flutter so that I can make it clear and simple because when I use real data in the future, I might have more than 2 cards in a group and they will also have different data. Any suggestion will be really appreciated.
class MyConstructor {
MyConstructor({this.jonathan1,this.jonathan2,this.jonathan3});
}
class StackedCardsState extends State<HomePage> {
List<MyConstructor> cards = [
MyConstructor(h1: "Hello", h2: "hello3")
];
/////
Padding(
padding: EdgeInsets.all(15.0),
child: Column(children: [
Text(MyConstructor.hey, style: TextStyle(fontWeight: FontWeight.bold),),
Text(MyConstructor.hey),
Text(MyConstructor.hey, style: TextStyle(color: Colors.red[500]),),
VerticalDivider(color: Colors.blue),
])),
Your problem is first of all rather simple, you are violating the DRY concept (Don't repeat yourself, https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ).
As soon as you start copy pasting code take a moment and think about your code and how you can abstract it into a reusable component.
Another big issue that I think you are lacking is variable naming. It is a very very important part of writing code. Might seem trivial but it will be very hard to understand what a variable named cardOne1 and cardTwo2 actually mean. What is the purpose of that variable? What does it do?
Now with that said I understand your app has something to do with car sales but other than that I'm not really sure what I'm looking at. There for I will have a harder time finding a good variable for this code but here is an example.
So lets break down the contents in the card to a single reusable widget, we can also make a data class (or model) for storing the data that we then give to the widget.
//car_details.dart
class CarDetails {
String title;
String diffNumber;
String diffPercent;
Color colorIndicator;
CarDetails({
this.title,
this.diffNumber,
this.diffPercent,
this.colorIndicator,
});
}
//car_card_details.dart
class CarCardDetails extends StatelessWidget {
final double padding;
final CarDetails carDetails;
CarCardDetails({
this.carDetails,
this.padding = 15,
});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
carDetails.colorIndicator != null
? Container(
color: carDetails.colorIndicator,
height: 60,
width: 2,
)
: Container(),
Padding(
padding: EdgeInsets.all(padding),
child: Column(children: [
Text(carDetails.title),
Text(carDetails.diffNumber),
Text(carDetails.diffPercent),
VerticalDivider(color: Colors.blue),
])),
],
);
}
}
To use this component we make a CarCard Widget that takes a title and a list of CarDetails like so:
// car_card.dart
class CarCard extends StatelessWidget {
final String title;
final List<CarDetails> carDetails;
CarCard({this.title, this.carDetails});
#override
Widget build(BuildContext context) {
List<Widget> detailRow = List();
if (carDetails != null) {
carDetails.forEach((element) {
detailRow.add(CarCardDetails(
top: element.title,
middle: element.diffNumber,
bottom: element.diffPercent,
lineColor: element.colorIndicator,
));
});
}
return Container(
//height: 150, //I would not hardcode the height, let the childrent expand the widget instead
child: SingleChildScrollView(
child: Card(
elevation: 8.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
child: InkWell(
child: Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Spacer(),
Icon(Icons.favorite)
]),
),
Divider(color: Colors.black),
Row(children: detailRow),
]),
),
),
),
);
}
}
And instead of saving all the variables you had in app we can now make them into a list of CarDetails where each element contains the strings.
// some other widget
...
List<CarDetails> carDetails = [
CarDetails(
title: "2 hrs ago",
diffNumber: "+/ TRACK",
diffPercent: "% to DBJ",
),
CarDetails(
title: "CHEVEROLET",
diffNumber: "-2706",
diffPercent: "42.2%",
colorIndicator: Colors.red,
),
CarDetails(
title: "BUICK",
diffNumber: "+300",
diffPercent: "50%",
colorIndicator: Colors.green,
),
CarDetails(
title: "GMC",
diffNumber: "-712",
diffPercent: "52.1%",
colorIndicator: Colors.black26,
),
];
#override
Widget build(BuildContext context) {
return CarCard(
title: "US Daily Retail Delieveries by Brand",
carDetails: carDetails,
);
}
...
This can of course be abstracted even further with the groups of cards etc, etc. But I hope you get the idea.
This is an example of how you could do it, with that said I do not know what data you are intending to use and how you want to structure it. So consider this a starting point and take it from there. :)

text with \n and unicode literals saved in mysql do not work when displayed

I store a text string with \n and unicode literals like \u2022 in mysql, then retrieve it with http api call on flutter. When displaying it with Text widget, these escaped symbles do not show as expected. When I directly pass the string , it works. Could anyone help me out?
child: Column(
children: <Widget>[
Text(prompt.prompt_body, //This variable is from http call which does not work
textAlign: TextAlign.left,
style:TextStyle(
color: Colors.black,
fontSize: 13,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic
)),
Divider(),
Text("You live in a room in college which you share with another student.However, there are many problems with this arrangement and you find it very difficult to work.\n\nWrite a letter to the accommodation officer at the college. In the letter,\n\n \u2022 describe the situation\n \u2022 explain your problems and why it is difficult to work\n \u2022 say what kind of accommodation you would prefer", //this part works
textAlign: TextAlign.left,
style:TextStyle(
color: Colors.black,
fontSize: 13,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic
))
],
),
emulator screenshot
In response to Gunter's query, I add the following code on api call:
class PromptModel {
int id;
String prompt_body;
String prompt_image;
PromptModel(this.id, this.prompt_body, this.prompt_image);
PromptModel.fromJson(Map<String, dynamic> parsedJson) {
id = parsedJson['id'];
prompt_body = parsedJson['prompt_body'];
prompt_image = parsedJson['prompt_image'];
}
}
....
class PromptListPageState extends State<PromptListPage> {
int counter = 0;
List<PromptModel> prompts = [];
void fetchImage() async {
counter++;
var response =
await get('http://10.0.2.2:8080/TestPrompt');
var promptModel = PromptModel.fromJson(json.decode(response.body));
setState(() {
prompts.add(promptModel);
});
}
The following is the response of the api call:
{"id":1,"prompt_body":"You live in a room in college which you share with another student.However, there are many problems with this arrangement and you find it very difficult to work.\\n\\nWrite a letter to the accommodation officer at the college. In the letter,\\n\\n \\u2022 describe the situation\\n \\u2022 explain your problems and why it is difficult to work\\n \\u2022 say what kind of accommodation you would prefer","prompt_image":"http://10.0.2.2:8080/test.jpg"}
I solved the problem by inputting the string from flutter using TextFormField. directly inserting the text on database side is tricky. The code is as below:
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: myController,
maxLines: 5,
validator: (val) =>
(val == null || val.isEmpty) ? "请输入商品名称" : null,
decoration: const InputDecoration(
//icon: Icon(Icons.person),
hintText: 'add the prompt here:',
labelText: 'Prompt content',
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.teal)),
),
onSaved: (val) => this.content = val,
),
new Container(
margin: const EdgeInsets.only(top: 10.0),
child: new RaisedButton(
onPressed: _save,
child: new Text('Save'),
),
)
]),
),
appBar: AppBar(
title: Text('Add Essay Prompt'),
),
),
);
}
}