Flutter vs Vue: Building a Card Component with Comparative Code
This article walks through the step‑by‑step implementation of a card UI component in both Flutter and Vue, explains the structural and styling differences, shows how to pass data and handle events, and concludes with a summary of Flutter’s widget‑centric approach, providing a practical learning resource for front‑end developers.
In the opening section the author explains why their team needs to migrate a Mini‑Program/H5 project to a native app and decides to learn Flutter, positioning the article as the first in a series that compares Flutter with traditional front‑end technologies.
The article then introduces a simple information card and states the goal: to implement the same layout in Flutter and Vue, focusing on the visual and structural differences rather than deep code theory.
Top Row Implementation
For the top part of the card (image, title, date, button) the Vue version uses a flex layout:
<template>
...
<div class="top-row">
<img src="./cat.jpg" alt="" />
<div class="card-content">
<div class="card-title">title</div>
<div class="card-date">date</div>
</div>
<div class="card-button">OPEN</div>
</div>
...
</template>
<style scoped>
.top-row { display: flex; align-items: center; }
.top-row > img { width: 80px; height: 80px; border-radius: 8px; }
.card-content { flex: 1; margin: 0 8px; min-width: 0; }
.card-title { font-size: 14px; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-date { font-size: 13px; color: gray; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-button { height: 32px; line-height: 32px; width: 74px; background: #401296; color: white; text-align: center; border-radius: 18px; font-size: 12px; }
</style>The Flutter counterpart builds the same row with widgets:
Widget buildTopRow(BuildContext context) {
return Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.asset('/assets/cat.jpg', width: 80, height: 80),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('title', maxLines: 1, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis),
Text('date', maxLines: 1, style: const TextStyle(color: Colors.grey), overflow: TextOverflow.ellipsis),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 10, 0),
child: FilledButton(onPressed: () {}, child: const Text('OPEN')),
),
],
);
}The author highlights that Row in Flutter corresponds to a flex container in CSS, and Expanded works like flex: 1 . Padding and ClipRRect replace CSS margin and border‑radius.
Bottom Row and Card Container
The bottom part (description and version/size) is similarly compared, with Vue using simple div elements and Flutter using a Column wrapped in Padding :
Widget buildBottomRow(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(model.appDescription),
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
child: Text("${model.appVersion} • ${model.appSize} MB"),
),
],
),
);
}The outer card container in Vue is a div with margin, background, border‑radius and box‑shadow, while Flutter uses a Container with BoxDecoration achieving the same visual effect.
Container(
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 8.0, spreadRadius: 2.0, offset: const Offset(2.0, 2.0)),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[buildTopRow(context), buildBottomRow(context)],
),
);Data Passing and Event Handling
Both frameworks use a model object to pass data into the card. Vue defines props and emits an onPressed event, while Flutter passes a CardItemModel instance via the constructor and provides a VoidCallback onPressed .
// Vue (TypeScript)
const emits = defineEmits(['onPressed']);
const props = defineProps<{ model: { appIcon: object; appName: string; appSize: string; appDate: string; appDescription: string; appVersion: string; } }>();
const model = unref(props.model); // Flutter (Dart)
class CardItemModel { ... }
class MyCardWithIcon extends StatelessWidget {
final CardItemModel model;
final VoidCallback onPressed;
const MyCardWithIcon({required this.model, required this.onPressed, required Key key}) : super(key: key);
...
}Full Application Example
The article finishes with a minimal main.dart that creates a MaterialApp , a Scaffold with an AppBar , and places MyCardWithIcon in the body, demonstrating the complete run‑time setup.
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(title: const Text('layout demo')),
body: MyCardWithIcon(
model: CardItemModel(
appIcon: 'assets/cat.jpg',
appDate: '2024-10-30',
appDescription: '...long description...',
appName: 'APP【APP】-... ',
appSize: '1',
appVersion: '1.0.0',
),
onPressed: () {},
),
),
);
}
}In the concluding remarks the author emphasizes Flutter’s core principle “everything is a widget”, noting that widgets are immutable and the UI is rebuilt when data changes, which simplifies state‑driven UI development.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.