Flutter Widget test cannot emulate different screen size properly - unit-testing

Before deploying my Flutter app, I wanted to test it on multiple screen sizes to check if there is any Renderflex overflow for smaller screens.
But I when first modified the screen size during widget testing to match the device I was using during the development, I realized that the widget test is throwing Render overflow errors already, even though it did not have such errors on the real device. So I asked this questions How to fix A RenderFlex overflowed during Widget Test
But I after further investigation and using Flutter golden feature test which snaps png out of widget tests, I narrowed down the problem to a discrepancy in text size.
You can see clearly in the reproducible step below that the text during the widget text is WAY BIGGER (on the right) than the actual text in the real device (on the left).
The bigger text size during Widget test causes the RenderFlex error in my app.
Steps to reproduce:
Now connect a real device and run this code with flutter run
lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: TextScaleComparaison(),
),
);
}
class TextScaleComparaison extends StatelessWidget {
#override
Widget build(BuildContext context) {
final widget = Scaffold(
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
final dpr = MediaQuery.of(context).devicePixelRatio;
final textScale = MediaQuery.of(context).textScaleFactor;
final vi = MediaQuery.of(context).viewInsets;
final vip = MediaQuery.of(context).viewPadding;
final font = DefaultTextStyle.of(context).style.fontFamily;
print("width is $width and height is $height and dpi is $dpr txtScale is $textScale vi is $vi vip is $vip font is $font");
return Center(child: Text("This cannot be that long!!"));
},
),
);
return widget;
}
}
Check the logs and you should see device screen info:
For me I got :
I/flutter (27450): width is 411.42857142857144 and height is 797.7142857142857 and dpi is 2.625 txtScale is 1.1 vi is EdgeInsets.zero vip is EdgeInsets(0.0, 24.0, 0.0, 0.0) font is Roboto
Copy the screen width and height to and textScale and devicePixelRatio to the next step in the code below.
Edit the code below to add the above setting because we want to simulate this exact screensize in the test.
test/test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {
testWidgets(
"Emulate real screen size",
(WidgetTester tester) async {
// Adjust these to match your actual device screen specs
final width = 414;
final height = 846;
tester.binding.window.devicePixelRatioTestValue = (2.625);
tester.binding.window.textScaleFactorTestValue = (1.1);
final dpi = tester.binding.window.devicePixelRatio;
tester.binding.window.physicalSizeTestValue = Size(width * dpi, height * dpi);
await tester.pumpWidget(
MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: TextScaleComparaison(),
),
),
);
await expectLater(
find.byType(TextScaleComparaison),
matchesGoldenFile("text.png"),
);
},
);
}
Run test.dart with flutter test --update-goldens test/test.dart
This will create a png file at test/text.png
Check the logs: For me it printed:
width is 414.0 and height is 846.0 and dpi is 2.625 txtScale is 1.1 vi is EdgeInsets.zero vip is EdgeInsets.zero font is Roboto
What I am missing ? Why can't the text show exactly the same as the real device?

That is because of the font difference used by flutter test and flutter run.
Flutter's default font is Roboto for Android if you did not change it other font.
Default Android: Roboto font and for iOS: San Francisco font
Customize https://flutter.dev/docs/cookbook/design/fonts
Either 1) or 2) these fonts are not available to flutter test by default.
Flutter test purposely uses a font called Ahem which is made out of square blocks that you see on your screenshot.
This is a preview:
Ahem font square are wayyy bigger than the normal that you are using. Therefore, it causes the RenderFlex overflow error
Solution
To achieve a near perfect emulation of your device in flutter test you have to download the font data then load the exact font that you are using.
To load a font in widget test, you should do inside the testWidgets function or setUp:
final flamante = rootBundle.load('assets/fonts/Flamante-Roma-Medium.ttf');
final fontLoader = FontLoader('FlamanteRoma')..addFont(flamante);
await fontLoader.load();
Then add this font to the ThemeData before pumping the widget.
theme: ThemeData(
fontFamily: 'FlamanteRoma',
),
The final test.dart code is:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/test/compare_test_size.dart';
void main() {
testWidgets(
"Emulate real screen size",
(WidgetTester tester) async {
final flamante = rootBundle.load('assets/fonts/Flamante-Roma-Medium.ttf');
final fontLoader = FontLoader('FlamanteRoma')..addFont(flamante);
await fontLoader.load();
// Adjust these to match your actual device screen specs
final width = 411.4;
final height = 797.7;
tester.binding.window.devicePixelRatioTestValue = (2.625);
tester.binding.window.textScaleFactorTestValue = (1.1);
final dpi = tester.binding.window.devicePixelRatio;
tester.binding.window.physicalSizeTestValue = Size(width * dpi, height * dpi);
await tester.pumpWidget(
MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: TextScaleComparaison(),
theme: ThemeData(
fontFamily: 'FlamanteRoma',
),
),
),
);
await expectLater(
find.byType(TextScaleComparaison),
matchesGoldenFile("text.png"),
);
},
);
}
Now re generate the golden test and check the png. You will see real text, not boxes anymore:
test/test.png
And don't forget to add the same font in your main.dart
runApp(
MaterialApp(
home: TextScaleComparaison(),
theme: ThemeData(
fontFamily: 'FlamanteRoma',
),
),
);
And also don't forget to update pubspec.yaml and run flutter pub get
- family: FlamanteRoma
fonts:
- asset: assets/fonts/Flamante-Roma-Medium.ttf

As Curly already mentioned here, you can overwrite the tester.binding.window.textScaleFactorTestValue with a lower value. 0.8 was working well for my used fonts, but depending on your setup, you might use even a lower value. In this way, you don't need to await the loading of your fonts in each test case.

Related

SwiftUICharts not changing color on bar chart

I am trying to build a watchOS app that has charting and I can't use Swifts built in charts because I need to support down to version 7 and swift charts are only available for watchOS 9+. So instead I am using a library I found here...
https://github.com/willdale/SwiftUICharts
It has some sample and examples, trying to follow them I was able to get the chart to show up and customize it some, but I can't get the bar chart items to change their color. It seems simple enough, but for whatever reason I can't get it to actually change the color.
I'll provide a simple sample that should help show what's going on.
struct ChartView: View {
var items: [TimeEntries] = [
TimeEntry(dateString: "01/23/2023", entry: 97, timestamp: Date().millisecondsSince1970)]
var body: some View {
let chartData = makeData(items)
BarChart(chartData: chartData)
.touchOverlay(chartData: chartData)
.padding()
}
private func makeData(_ items: [TimeEntries]) -> BarChartData {
var data: [BarChartDataPoint] = [BarChartDataPoint]()
for item in items {
let stat = BarChartDataPoint(
value: Double(item.entry),
xAxisLabel: "Wed",
date: Date(milliseconds: entry.timestamp),
colour: ColourStyle(colour: Color.purple)
)
data.append(stat)
}
let dataSet = BarDataSet(dataPoints: data)
return BarChartData(dataSets: dataSet)
}
}
That should give me an entry on my bar chart with purple filling, I simplified this for sake of ease of posting, my real data has 7 points in it.
However, what actually happens is I have a single red bar on my chart. I am not using red anywhere in the app at all, but it won't take the color that I specify in the colour property of the BarChartDataPoint.
I know it's based on a library, but hopefully someone here will have used this library and will know what I have done wrong. I'll attach a screenshot of the chart so you can see. Thank you.

Expo Font only displaying custom font in first screen on Android. iOS is working properly

My app is running Expo Font correctly on iOS but not on Android. I've also tried replacing loadAsync with useFonts according to the docs to no avail. I have two separate stylesheets with a baseFont property setting the custom font, but the font is not being read properly for some reason on the other screens. The non-login screens are located in different stacks using React Stack Navigation (#react-navigation/stack). This is basically what the code looks like:
App.js:
import RootStack from './navigations/RootStack';
import mainReducer from './reducers/index';
import { getAsyncStorage } from './actions/AuthActions';
const store = createStore(mainReducer, applyMiddleware(thunk));
store.dispatch(getAsyncStorage());
export default () => {
const [fontLoaded, setFontLoaded] = useState(false);
async function loadResourcesAsync() {
await Promise.all([
Font.loadAsync({
'custom-font': require('./assets/fonts/custom_font.ttf');
}),
]);
setFontLoaded(true);
}
useEffect(() => {
loadResourcesAsync();
});
if (!fontLoaded) return (null);
return (
<Provider store={store}>
<RootStack />
</Provider>
);
};
styles/Login.js:
const styles = StyleSheet.create({
baseFont: {
fontFamily: 'custom-font',
},
Turns out the answer was particular to fontWeight in a stylesheet. Since the custom font didn't have a bold typeface embedded in the .ttf, I had to download the bold version and remove fontWeight: bold from the css.

Remember Scroll Position in very long ListView

I have a very long list of items being shown in my Flutter app, the list is being populated by an API and the user can opt to refresh the list to get the latest information.
Is there any way to know where in the list the user has scrolled to at any particular time?
Then when the user presses refresh I can then scroll back to the latest place in the list they were looking at?
I trued using this "scrolled position list" but when I scrolled the frame rate was dropping a LOT...down to 20 fps, I want a nice smooth 60fps if possible.
https://pub.flutter-io.cn/packages/scrollable_positioned_list
Have you tried running your app with --release ? Release mode has a better performance than debug mode. And I think you can just use a normal ListView.builder() with a ScrollController https://api.flutter.dev/flutter/widgets/ScrollController-class.html Maybe that has better performance than the scrollable List on pub.dev
I don't know the scrollable_positioned_list package itself, but I don't think you need it for your implementation.
First of all, for long lists it is important to use ListView.builder() for performance reasons. This will only load the items that are currently visible on your screen. This saves a lot of resources (this is the equivalent of the RecyclerView in Android).
To get the position of a list, you can save the scroll offset value of the list. You can get this via a ScrollController.
Here is my example code:
import 'package:flutter/material.dart';
import 'package:preferences/preference_service.dart';
class MyListView extends StatefulWidget {
final String _prefKey = 'listViewOffset';
#override
_MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
ScrollController controller;
#override
void initState() {
controller = ScrollController(initialScrollOffset: PrefService.getDouble(widget.prefKey);
controller.addListener(() {
PrefService.setDouble(widget.prefKey, controller.offset);
});
super.initState();
}
#override
Widget build(BuildContext context) {
throw ListView.builder(
controller: controller,
itemBuilder: (BuildContext context, int position) {
return ListTile(title: Text("Position $position"));
}
);
}
#override
void dispose() {
controller.dispose(); // remove the listeners of our controller
super.dispose();
}
}
To save the offset I use the package preferences but you can use the shared_preferences package as well.
Unfortunately I didn't get the chance to test my code in a finished app, but I hope it does what it should.

ChartJS custom tooltip doesn't render background on labels (only the title)

Using ChartJS, I want to be able to change the title on a tooltip depending on the data (mainly as I want the text in a smaller font size than the label). I don't really need a full custom HTML tooltip, just be able to change fontsize and title text.
However just setting this via a "custom" callback means the label for the dataset doesn't have the background correctly displayed
options: {
tooltips: {
custom : t => {
t.title = ['Hello'];
}
}
}
See this JSFiddle: https://jsfiddle.net/MrPurpleStreak/2n8md9Lh/
Hover over a point and see the "hello" on a black background, but the data not.
NOTE: I've found a way to accomplish my initial goal, but this struck me as a bug in chartJS?
There seems to be an issue with the custom property.
I recommend using the callbacks instead :
tooltips: {
displayColors: false,
backgroundColor: 'rgb(0,0,0,1)',
callbacks: {
title: function(tooltipItems, data) {
return 'Hello';
},
}
}
See jsFiddle

qml desktop components scaling

I want to create a user interface qtquick2 that can be scaled and includes some desktop components. As mentioned in this blogpost the default rendering for qml/qtquick2 should use distance fields and not native text rendering. I tried to scale qt quick controls. The result is rather disappointing. I was testing on ubuntu 64 and qt-5.1.1. The text on the controls is looking bad but all text in standard qml elements (Text/TextEdit) is looking good when scaled.
This leads me to think that native rendering is the default now for desktop components. Can this be turned of?
Setting render types of Qt Quick Controls will be available in Qt 5.2 using styles, e.g. in TextArea:
TextArea {
/* ... */
style: TextAreaStyle {
renderType: Text.QtRendering
}
}
Supported render types are:
Text.QtRendering
Text.NativeRendering (default)
See TextArea.qml, TextAreaStyle.qml.
For Button and ButtonStyle there is no public interface to set the render type directly in Qt 5.2. But what you can do, is overwrite the label with your own text component:
Button {
id: theButton
/* ... */
style: ButtonStyle {
label: Item {
implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight
property var __syspal: SystemPalette {
colorGroup: theButton.enabled ?
SystemPalette.Active : SystemPalette.Disabled
}
Row {
id: row
anchors.centerIn: parent
spacing: 2
Image {
source: theButton.iconSource
anchors.verticalCenter: parent.verticalCenter
}
Text {
renderType: Text.NativeRendering /* Change me */
anchors.verticalCenter: parent.verticalCenter
text: theButton.text
color: __syspal.text
}
}
}
}
This code is inspired by the default label component of ButtonStyle.qml, modified and untested.
I don't think you can change text rendering in Qt Components since they are explicitly made for the use in desktop applications.
In TextArea for example there is no renderType like in TextEdit.
On the QtDesktopComponents page I another hint:
You have to change QGuiApplication to a QApplication. This is because the components rely on certain widget-specific classes such as QStyle to do native rendering.