Hello! Welcome to your first project in this course. Over the last few lessons, we've laid the groundwork by understanding Dart fundamentals and the core principles of Flutter's declarative UI. You've become familiar with the essential widgets for displaying content and handling user input. Now, it's time to assemble those pieces into a complete, functional mini-application.
Today, we will build a 'Digital Business Card' app. This project is designed to solidify your understanding of UI composition and introduce two critical new skills: fetching live data from an internet API and managing state to create an interactive user experience.
Our goal is to build an app that looks something like this:

A target design for our Digital Business Card app, showcasing a profile picture, name, title, and contact information in a clean, stylish layout.
To achieve this, we'll follow a standard three-step workflow that is fundamental to most data-driven applications.

This diagram shows the typical data flow in a Flutter app: fetching raw data, parsing it into structured Dart objects, and then rendering it on the screen.
By the end of this lesson, you will have built a complete app that fetches, parses, and displays data, and responds to user interaction—a major milestone in your app development journey.
Step 1: Project Setup and Data Modeling
A well-organized project is easier to build, debug, and scale. We'll start by setting up a clean folder structure and defining a "model" for our data. A data model is simply a class that defines the structure of the data we expect to receive from the API. This practice enforces type safety and makes your code more readable and robust, much like defining an interface or type in TypeScript.
We'll use a free mock API called Random User Generator to get our business card data. A typical response for one user looks like this (simplified):
{
"results": [
{
"name": {
"first": "Ehmad",
"last": "Saeed"
},
"email": "ehmad.saeed@example.com",
"phone": "+92 321 1234567",
"picture": {
"large": "https://randomuser.me/api/portraits/men/75.jpg"
}
}
]
}
Our task is to fetch this JSON and convert it into a structured Dart object. Let's get our project ready for this.
REST API in Flutter: A beginner's guide to fetching data
This Codecademy article provides an excellent guide on structuring a Flutter app for API calls. We will follow its recommendations for project setup and data modeling.
First, follow the steps in the Project setup section. Create a new Flutter project, and inside the lib directory, create three subfolders: models, services, and ui. Then, add the http package to your pubspec.yaml as instructed. This package is the standard, lightweight way to make HTTP requests in Dart. Next, read the section Understanding the API and creating the model. Instead of the Post model shown in the article, we will create a User model that matches the randomuser.me API data. In your models folder, create a file named user.dart and add the following code:
```dart
// In lib/models/user.dart
class User {
final String name;
final String email;
final String phone;
final String pictureUrl;
User({
required this.name,
required this.email,
required this.phone,
required this.pictureUrl,
});
// This is a 'factory constructor'. It's a common pattern for
// creating an instance of a class from a JSON object.
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: '${json['name']['first']} ${json['name']['last']}',
email: json['email'],
phone: json['phone'],
pictureUrl: json['picture']['large'],
);
}
}
```
Take a moment to analyze this class. It defines the structure of our user data and includes a helpful `fromJson` constructor to handle the conversion from raw JSON. This pattern is analogous to having a dedicated function or class method for object instantiation and mapping in other languages.
Step 2: The Networking Service
With our model in place, the next step is to create a dedicated "service" layer. The sole responsibility of this service is to handle communication with the API. This separation of concerns is a core principle of clean software design: the UI shouldn't know how the data is fetched, only that it can be requested.
This will feel very similar to using fetch in JavaScript. The Future returned by our service method is Dart's equivalent of a JavaScript Promise.
REST API with Flutter | Step by step tutorial
The video "REST API with Flutter" demonstrates how to create a clean service layer for making API calls. We'll adapt its approach to fetch our user data.
Watch the section from creating a remote service. This part clearly explains how to create a service class, add the http package, make an asynchronous GET request, check the response status, and parse the JSON body. Based on what you've just watched, create a file remote_service.dart in your services folder and add the following code. This implementation is tailored for our specific API.
Based on what you've just watched, create a file remote_service.dart in your services folder and add the following code. This implementation is tailored for our specific API.
// In lib/services/remote_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/user.dart';
class RemoteService {
Future<User?> getUser() async {
var client = http.Client();
var uri = Uri.parse('https://randomuser.me/api/?inc=name,email,phone,picture');
var response = await client.get(uri);
if (response.statusCode == 200) {
var json = jsonDecode(response.body);
// The API returns a list under the 'results' key
var userJson = json['results'][0];
return User.fromJson(userJson);
} else {
// Handle the error or return null
return null;
}
}
}
Notice how we use http.get, await the response, perform a status check, and then use jsonDecode to parse the string. Finally, we use our User.fromJson factory constructor to create a clean, type-safe User object.
Step 3: Building an Asynchronous UI with FutureBuilder
Now we need to connect our service to the UI. A common challenge is handling the time it takes for the network request to complete. We need to show the user a loading indicator while we wait for data and then display the data once it arrives.
Flutter has a wonderful widget for this exact purpose: FutureBuilder. It takes a future (our getUser() method) and a builder function. The builder provides us with the state of the future (snapshot), allowing us to easily return different widgets for different states:
ConnectionState.waiting: The future is still in progress. We show a loading spinner.ConnectionState.done: The future has completed. We check if we have data or an error and show the appropriate UI.
This declarative approach is very powerful and simplifies async UI logic immensely compared to manually managing loading flags.
Let's implement this in our main screen. Create a file named business_card_page.dart in a new folder called ui.
// In lib/ui/business_card_page.dart
import 'package:flutter/material.dart';
import '../services/remote_service.dart';
import '../models/user.dart';
class BusinessCardPage extends StatefulWidget {
const BusinessCardPage({super.key});
@override
State<BusinessCardPage> createState() => _BusinessCardPageState();
}
class _BusinessCardPageState extends State<BusinessCardPage> {
// A variable to hold the Future we want to build the UI from.
late Future<User?> _userFuture;
@override
void initState() {
super.initState();
// Start fetching the user when the widget is first created.
_userFuture = RemoteService().getUser();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Digital Business Card'),
),
body: Center(
child: FutureBuilder<User?>(
future: _userFuture,
builder: (context, snapshot) {
// Case 1: The future is still running
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
// Case 2: The future has completed with an error
else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
// Case 3: The future has completed successfully with data
else if (snapshot.hasData) {
final user = snapshot.data!;
// We'll build our card widget here in the next step
return Text('Name: ${user.name}');
}
// Case 4: The future completed with no data (or is null)
else {
return const Text('No user data found.');
}
},
),
),
);
}
}
Finally, update your main.dart to show this new page:
// In lib/main.dart
import 'package:flutter/material.dart';
import 'ui/business_card_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Digital Business Card',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const BusinessCardPage(),
);
}
}
Run your app now. You should see a loading spinner for a moment, followed by the user's name displayed on the screen. We have successfully fetched and displayed live data!
Step 4: Composing the Card and Adding Interactivity
With the data flowing, we can now focus on building the polished UI from our design goal. This will primarily use the layout widgets you've learned about (Container, Column, Row) and the state management concepts needed for the "tap-to-reveal" feature.
For this interactivity, we'll use a StatefulWidget. The _BusinessCardPageState we already created is perfect for this. We'll add a boolean flag, _showContactInfo, to its state. Tapping a part of the UI will call setState(), which toggles this flag and tells Flutter to rebuild the widget with the updated information. This mechanism is Flutter's equivalent of React's useState hook; calling the setter function triggers a re-render.
Building a card widget in Flutter - LogRocket Blog
This article provides an excellent, detailed walkthrough of how to build a custom card layout by combining Column, Row, CircleAvatar, and Text widgets. We will use it as our guide.
Focus on the section Determining card widget layout. While the article builds a complete, self-contained widget, we will integrate its layout principles directly into our FutureBuilder. Replace the return Text('Name: ${user.name}'); line in your business_card_page.dart with the full card layout. Here is the complete code for _BusinessCardPageState, incorporating both the layout and the tap-to-reveal logic:
Replace the return Text('Name: ${user.name}'); line in your business_card_page.dart with the full card layout. Here is the complete code for _BusinessCardPageState, incorporating both the layout and the tap-to-reveal logic:
// In lib/ui/business_card_page.dart
class _BusinessCardPageState extends State<BusinessCardPage> {
late Future<User?> _userFuture;
bool _showContactInfo = false; // State for interactivity
@override
void initState() {
super.initState();
_userFuture = RemoteService().getUser();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.teal, // Set a background color
appBar: AppBar(
title: const Text('Digital Business Card'),
backgroundColor: Colors.teal[700],
),
body: Center(
child: FutureBuilder<User?>(
future: _userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator(color: Colors.white);
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.white));
} else if (snapshot.hasData) {
final user = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircleAvatar(
radius: 50.0,
backgroundImage: NetworkImage(user.pictureUrl),
),
const SizedBox(height: 10.0),
Text(
user.name,
style: const TextStyle(
fontFamily: 'Pacifico', // Add this font in pubspec.yaml for extra style!
fontSize: 40.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
'FLUTTER DEVELOPER',
style: TextStyle(
fontFamily: 'Source Sans Pro', // Also add this font
color: Colors.teal.shade100,
fontSize: 20.0,
letterSpacing: 2.5,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 20.0,
width: 150.0,
child: Divider(color: Colors.teal.shade100),
),
GestureDetector(
onTap: () {
setState(() {
_showContactInfo = !_showContactInfo;
});
},
child: _showContactInfo
? Column(
children: [
InfoCard(text: user.phone, icon: Icons.phone),
InfoCard(text: user.email, icon: Icons.email),
],
)
: InfoCard(text: 'Tap to view contact info', icon: Icons.touch_app),
),
],
),
);
} else {
return const Text('No user data found.', style: TextStyle(color: Colors.white));
}
},
),
),
);
}
}
// A helper widget to reduce code duplication for the info cards
class InfoCard extends StatelessWidget {
final String text;
final IconData icon;
const InfoCard({super.key, required this.text, required this.icon});
@override
Widget build(BuildContext context) {
return Card(
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
child: ListTile(
leading: Icon(
icon,
color: Colors.teal,
),
title: Text(
text,
style: TextStyle(
color: Colors.teal.shade900,
fontFamily: 'Source Sans Pro',
fontSize: 18.0,
),
),
),
);
}
}
Study this final code carefully. Notice how the GestureDetector wraps the contact section. Its onTap callback triggers setState, which toggles our boolean flag. The conditional expression (_showContactInfo ? ... : ...) then rebuilds the UI to either show the contact details or the "tap to reveal" prompt. This is declarative UI and state management in action.
Conclusion
Congratulations on building your first complete Flutter app! You've successfully navigated the entire process from project setup to a polished, interactive, data-driven UI.
Let's recap the key skills you've put into practice:
- Project Structuring: You organized your code into
models,services, anduifolders for better maintainability. - Networking: You used the
httppackage to make an API call and parsed the JSON response into a type-safe Dart model. - Asynchronous UI: You learned to use the
FutureBuilderwidget to declaratively handle loading, error, and data states, creating a smooth user experience. - UI Composition: You combined
Column,CircleAvatar,Text, and customCardwidgets to build a complex layout. - State Management: You used a
StatefulWidgetand thesetStatemethod to manage and react to user interaction, implementing the tap-to-reveal feature.
This project forms a solid foundation for almost any app you will build. In our next module, we'll dive deeper into Advanced UI. You'll learn how to build more complex layouts, create a consistent app theme, and craft beautiful animations to bring your user interfaces to life.