How I Used Flutter to Build a Fully-Functional Carousell Clone
Carousell is a popular marketplace app that allows users to buy and sell a variety of used items. In this blog, I will walk through how I built a fully functional clone of the Carousell app using Flutter. If you are interested in building a classified platform then choose Zipprr’s script https://zipprr.com/carousell-clone/
Project Planning
The first step was planning out the key features and functionality needed to replicate the core Carousell experience. I determined the main features should include:
- Listing browse functionality to view categories and listings
- User profiles to view other users’ listings and activity
- Listing create/edit functionality to sell items
- Wishlist functionality to save items
- Chat/Messages between buyers and sellers
- Login/Registration for user accounts
- Payment integration to complete purchases
In terms of architecture, I decided to use the BLoC pattern for state management along with Firebase for authentication, data storage and remote configs.
For designing the UI, I planned to leverage Flutter’s widget library as much as possible while also creating some reusable custom widgets for common UI patterns. Some common screens include listings browse, profile, listings create/edit etc.
Setting Up Flutter Development Environment
The next step was setting up my development environment. I installed the latest stable Flutter SDK and Android Studio/VSCode on my machine.
To install Flutter, I ran the following commands:
flutter doctor
flutterconfigThis installs the necessary CLI tools and also configures PATH variables.
Then I created a new Flutter project within Android Studio:
File > New > New Flutter ProjectI named it ‘carousell_clone’ and selected the default Flutter Application template with Material Design components.
This generated the basic project structure with lib, test and android/ios folders. The lib folder contains the Dart code while android folder contains generated android project files.
Designing UI with Flutter Widgets
With the project setup complete, the next step was designing key screens using Flutter widgets.
For the home/browse screen, I used a Scaffold widget with AppBar and bottom navigation bar. The body contained a ListView to display listings:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
bottomNavigationBar: BottomNavigationBar(),
body: ListView(
children: [
// Listing Cards
],
),
);
}
}For individual listing cards, I used the Card widget along with child widgets like Image, Title, Price etc:
Card(
child: Column(
children: [
Image.network(listing.imageUrl),
Text(listing.title),
Text("\$${listing.price}")
]
)
)Similarly, I designed other screens like Profile, Chat, Payments etc using common Flutter widgets like GridView, Column, TextFormFields etc.
Developing Reusable Custom Components
To promote reusability, I extracted out common UI patterns into custom widgets.
For example, the listing card design was extracted to a custom ListingCard widget:
class ListingCard extends StatelessWidget {
final Listing listing;
const ListingCard({
Key key,
@required this.listing
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(...);
}
}Similarly, I created ProfileBanner, ContactBox, AvatarWidget etc which were reused across multiple screens.
This reduced duplication and makes the codebase more maintainable. Whenever designs change, only the custom widgets need updating.
Working with State Management
For managing app state, I implemented the BLoC pattern using the flutter_bloc package.
I defined Bloc classes for each state-impacting section like authentication, listings, messaging etc.
For example, the AuthenticationBloc:
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
@override
AuthenticationState get initialState => AuthenticationUninitialized();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
// Handle login, logout, register events
}
}Events trigger stream changes which are consumed by widgets using BlocProvider/BlocBuilder.
For listings, I created ListingBloc with getListing, filterListings() on ListingLoaded event:
yield ListingLoading();
try {
final listings = await listingRepository.getListings();
yield ListingLoaded(listings);
} catch (err) {
yield ListingError(err);
}This centralized and organized state management logic.
Implementing Navigation
I used Navigator 2.0 for handling navigation transitions. MaterialPageRoute was used for named routes and push/pop:
onPressed: () {
Navigator.pushNamed(context, '/profile');
}Routes were defined in main.dart:
routes: {
'/': (_) => HomeScreen(),
'/profile': (_) => ProfileScreen()
}For nested navigators like drawer/bottom bar, I used Navigator wrapped children which managed independent route stacks:
Navigator(
onGenerateRoute: (_) => MaterialPageRoute(
builder: (_) => HomeScreen()
)
)This allowed seamless navigation between all screens.
Fetching and Displaying Remote Data
I integrated the Dio HTTP client package to make API calls and fetch/update data from a remote Fake API.
Get Zipprr’s stories in your inbox
Join Medium for free to get updates from this writer.
For listings, an API call was made on ListingLoaded event:
Future<List<Listing>> getListings() async {
final response = await Dio().get('listings');
return (response.data as List)
.map((listing) => Listing.fromJson(listing))
.toList();
}The response was parsed to Listing models and returned.
Similarly, other endpoints were called on corresponding Bloc events like register, login etc. and responses handled accordingly.
Error cases were handled by yielding error states from bloc.
This allowed remote data fetching and updating app state based on API responses.
Implementing User Authentication
Firebase Authentication was used for managing user logins/signup.
On register event, auth was called and then state updated:
await _auth.createUserWithEmailAndPassword(
email: user.email,
password: user.password
);
// Update state
yield AuthenticationSuccess(user);Login worked similarly by signing in and updating state.
FirebaseUser data was used to populate and retrieve user profiles.
Integrating auth allowed implementing features dependent on login state like posting listings, messaging etc.
User state was propagated to widgets using BlocProvider for secure access control.
Creating Database with Firestore
Cloud Firestore was used as the backend database for storing app data.
Collections were created for listings, users, chat etc. Documents represented individual objects.
Listing data was saved on post event:
await listingCollection.doc().set({
'title': listing.title,
'price': listing.price,
//...
});Retrieval was done by getting all docs from collection:
final snapshots = await listingCollection.get();
return snapshots.docs.map((doc) => ...)This allowed storing, updating and syncing data across clients in realtime.
Collections were modeled based on requirements for flexible querying.
Publishing App to Play Store
Once development was complete, the next step was to publish the app.
I generated a signed release APK from Android Studio:
Build > Generate Signed Bundle/APKThis produces optimized APK for production with metadata included.
I then created a new app listing on the Play Console providing details like:
- App name, description, screenshots etc.
- Pricing as free
- App content rating
Finally, I uploaded the signed APK and published the app.
Within a few hours, the app went live on the Play Store and was discoverable globally!
Future Improvements
Some features that can be added in the future include:
- Chat functionality with realtime messaging
- Payments integration using Flutterwave/Paystack
- Push notifications for new messages/listings
- Search listings functionality
- Location-based listings filtering
- Seller/buyer reviews and ratings
Some lessons learned were optimizing app bundle size, implementing crashlytics for errors, releasing frequent updates based on feedback.
Overall, Flutter proved to be an incredibly powerful tool for building high-quality native mobile applications using a single codebase.










