P2 — From React to Flutter Web

Finally second part is out of my mini series of moving to Flutter Web. To understand whole project, you can visit Part 1 of this series.

Short recap

I’ve a website which I migrate from React to Flutter Web. Structure of the website consists of AppBar and different sections. AppBar and SectionOne is covered in Part 1 of this series. In Part 2 we will be covering second section which is BenefitsSection.

BenefitsSection

Left — Look on Web, Right — Look on Mobile

Little redundancy , but I’ll remind our MainSection code where all sections are being written.

class _MainSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(22),
children: <Widget>[
Column(children: <Widget>[
_SectionOne(),
Container(
transform: Matrix4.translationValues(0.0, -140.0, 0.0),
child: _BenefitsSection(..),
_BenefitsSection(..),

_SectionThree(),
_BenefitsSection(..),
_LanguageSection(),
_AboutSection(),
_PeopleSection(),
_FooterSection()
],
);
}
}

We will get deeper into first benefit sections. Later benefit section repeats itself, therefore everything can be covered now.

Please notice, that first Benefit Section does have Container as a parent. It is used only for transform need. I wanted design to be as precise as possible, and I noticed that first benefit section has to overlap previous section therefore I applied transformation to it. Now section is moved from its current position to 140px up.

And now the best part — BenefitsSection StatelessWidget!

class _BenefitsSection extends StatelessWidget {
final bool isMirrored;
final ListData listData;

const _BenefitsSection({Key key, this.isMirrored = false, @required this.listData}) : super(key: key);

@override
Widget build(BuildContext context) {
if (MediaQuery.of(context).size.width < ScreenSizeHelper.SCREEN_WIDTH_MEDIUM) {
return Column(
children: <Widget>[getImage(context), getContainer(context)],
);
} else {
if (isMirrored) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getContainer(context)), Expanded(child: getImage(context))],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getImage(context)), Expanded(child: getContainer(context))],
);
}
}
}

Widget getContainer(BuildContext context) {
ButtonData _buttonData = listData.buttonData;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
width: 500,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AutoSizeText(
listData.mainTitle,
style: Styles.kCheckedItemMainTitle,
minFontSize: Styles.kCheckedItemMainTitle.fontSize,
maxLines: 6,
),
for (var item in listData.checkedItems)
_CheckedItemWidget(title: item.title, subtitle: item.subtitle),
if (_buttonData != null)
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RentMiButton(
color: RentMiColors.kRentMiRed.value,
text: _buttonData.title,
onButtonClickListener: () {
String url = _buttonData.url;
if (url.isEmpty) {
Common.contactRentMi("I would like to ask you some questions");
} else {
Common.openWebsite(_buttonData.url);
}
}),
],
),
)
],
),
),
);
}

Widget getImage(BuildContext context) {
return Image.asset(
listData.image,
height: 600,
);
}
}

For it I has two parameters — isMirrored and ListData.

isMirrored is a boolean flag. As you noticed from representation of Benefits Section, there are two kinds of it. They both consists of same UI elements but they are put in different directions. That’s where I use isMirrored flag.

if (isMirrored) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getContainer(context)), Expanded(child: getImage(context))],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getImage(context)), Expanded(child: getContainer(context))],
);
}
}

If it’s true, then I show Container and Image Widgets, otherwise I show Image and Container Widgets. Everything is a widget (i’m not sure how many times I heard it and how many times I said that 😅).

To differentiate if screen is mobile or not, we use same MediaQuery widget. And this section is straightforward — if screen is mobile (less than my defined value) then return elements vertically, otherwise — horizontally. And take into consideration isMirrored flag.

@override
Widget build(BuildContext context) {
if (MediaQuery.of(context).size.width < ScreenSizeHelper.SCREEN_WIDTH_MEDIUM) {
return Column(
children: <Widget>[getImage(context), getContainer(context)],
);
} else {
if (isMirrored) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getContainer(context)), Expanded(child: getImage(context))],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Expanded(child: getImage(context)), Expanded(child: getContainer(context))],
);
}
}
}

For regular desktop screen, I put my widgets into Expanded widget. All it does it utilises maximum amount of space within the screen.
I cannot do the same with mobile screen, because the parent is Column and column doesn’t have vertical ending therefore I’d get an error if I tried to wrap it to Expanded widget.

These things takes time to grasp in Flutter. Practice practice practice!

ListData is our model class which holds variety of things.

class ListData {
String mainTitle;
String image;
ButtonData buttonData;
List<CheckedItem> checkedItems;

ListData({@required this.mainTitle, @required this.image, this.buttonData, @required this.checkedItems});
}

class ButtonData {
String title;
String url;

ButtonData(this.title, this.url);
}

class CheckedItem {
String title;
String subtitle;

CheckedItem(this.title, this.subtitle);
}

Very explanatory. I love dart’s @required annotation. It simply makes sure, when creating constructor, you do have this value provided.
ListData has naming arguments. Only these ones can be annotated as @required because these arguments can be written in any order.

I see tendency that people create Maps. For example to get title of CheckedItem button we would write:

listData['checkedItem']['title']

I generally don’t like it because it’s very easy to make spelling mistakes. And generally make some silly mistakes. I prefer making models for that purpose.

Last step is to create a BenefitSection widget with mentioned parameters

_BenefitsSection(
isMirrored: true,
listData: ListData(
mainTitle:
"Found your dream home but need someone to share rent with? Find the perfect roommate with RentMi",
image: "assets/images/phone_case_finder.png",
buttonData: ButtonData("Find your match now", "https://www.facebook.com/groups/RentMiRoommate"),
checkedItems: List<CheckedItem>()
..add(CheckedItem("Quick set-up", "post a pic, write a little bio - that’s all you need!"))
..add(CheckedItem("Have fun browsing", "swipe till you get the perfect match"))
..add(CheckedItem("Connect together", "once matched, start chatting!"))))

Very easy and straightforward. Again!

I love to reuse code. That’s why I created BenefitsSection in the first place because I noticed, elements are being reused. getContainer() method is same case.

Widget getContainer(BuildContext context) {
ButtonData _buttonData = listData.buttonData;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
width: 500,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AutoSizeText(
listData.mainTitle,
style: Styles.kCheckedItemMainTitle,
minFontSize: Styles.kCheckedItemMainTitle.fontSize,
maxLines: 6,
),
for (var item in listData.checkedItems) _CheckedItemWidget(title: item.title, subtitle: item.subtitle),
if (_buttonData != null)
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RentMiButton(
color: RentMiColors.kRentMiRed.value,
text: _buttonData.title,
onButtonClickListener: () {
String url = _buttonData.url;
if (url.isEmpty) {
Common.contactRentMi("I would like to ask you some questions");
} else {
Common.openWebsite(_buttonData.url);
}
}),
],
),
)
],
),
),
);
}

Generally, this Widget consists of Column which has title, several CheckedItems widget and the button.

For texts I use AutoSizeText lib. I find it super helpful for desktop screens. Once people start to resize them, text can feel more consistent.

You can see lots of reusability right there. Several styles, buttons, etc.
If you are still hanging on reading this, please, try to reuse everything in your code. And keep yourself and your future colleague happy!

As for the button, we have to check if our ButtonData object exists. If not — don’t even show it.

Reuse reuse reuse 😆 Another Widget which is reused for this purpose. And whole styling mechanics are being reused there too!

class _CheckedItemWidget extends StatelessWidget {
final String title;
final String subtitle;

const _CheckedItemWidget({Key key, this.title, this.subtitle}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Table(
columnWidths: {0: IntrinsicColumnWidth()},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.check_circle,
color: RentMiColors.kRentMiBlue,
size: 16,
),
),
AutoSizeText(
title,
style: Styles.kCheckedItemWidgetTitle,
minFontSize: Styles.kCheckedItemWidgetTitle.fontSize,
maxLines: 2,
),
],
),
TableRow(
children: [
Container(),
AutoSizeText(subtitle,
style: Styles.kCheckedItemWidgetDescription,
minFontSize: Styles.kCheckedItemWidgetDescription.fontSize,
maxLines: 4)
],
)
]),
);
}
}

Structure is very simple. We have a table, with rows. Which correctly align to our purpose.
We reuse our styles to be consistent. And take provided values from ListData model.

One last widget method is getImage(). And probably most complicated one.
Just kidding. Image.asset widget in its simplest form!

Widget getImage(BuildContext context) {
return Image.asset(
listData.image,
height: 600,
);
}

..aaand that’s it folks! With some thinking, we created many reusable things which we can use through-out our code all the time!

Hopefully you can find this helpful and see you in the upcoming, Part 3! Feel free to leave your comments/questions/suggestions! And let me know, what would you like to feel digging deeper into!

Cheers

I build kick-ass mobile apps @ https://isawthatguy.com || Product Virtuoso and Startup Freak

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store