How I built a social network like Instagram in a few lines of code using flutter and why I fell in love with it
** This project tutorial is OUT OF DATE. It was created with a very early version of flutter and I am not able to maintain it **
As of last year I had very fun building my own little social network called Lime. So far you can post images and text messages to Lime, give posts a "Like" and comment/chat on them. The clue: All messages are tightly bound to the location where it was posted. That means you are only able to see posts which have been created in a maximum radius of 30km. Here is what it currently looks like:
But the App had one big problem: I didn't develop an iOS Version(so far)!
So because I do not own an Apple Computer nor do I own an iPhone I decided to look for any cool
Framework which would allow me to develop Lime for both platforms. This way I would be able to build
and debug it on my linux machine and my Android smartphone, which sounded great to me
Well there are many reasons:
- First of all I like Dart and I prefer it MUCH over javascript since I am most used to writing Java code
- Secondly IntelliJ is my IDE of choice ❤️
- And the Game-Changer (for me): Flutter runs inside a VM and draws everything itself. This should bring performance and a whole lot of possibilities to the framework. And yep: I was right!
Since I started building Lime with Flutter it took me around 10 hours of work (reading the docs included) to built the following and here is how its done.
Disclaimer: I assume, that you already know how to setup Flutter and that you have a very basic understanding of how the Framework works. There is a lot of great stuff to read for you at https://flutter.io ❤️
We will rebuild the basic layout of lime including the ViewPager and the bottom navigation.
Since Lime includes multiple lists, displaying large data-sets, should we implement a way to handle
the data loading and pagination logic in a somewhat elegant way. I will show how I did it. You
can judge it if you want
We will rebuild the layout of a post. We will see how easy even non-trivial layouts can be built using Flutter and how fast it is done.
We want the images to fade after loading. I will show how you can use Flutters modular system of widgets to build such a behaviour in very few lines of code
Once a user hits the little ghost the post is liked. I will show how to execute a simple REST-API call, and how to animate the state change. We will learn about callbacks to handle state changes of parent widgets.
The first step is done by rebuilding the basic layout of Lime. We can obviously see that the App consists of:
- A toolbar (which we will miss in this Article since i did not build it yet 😃
- A ViewPager(Android)/PageView(Flutter) containing three Pages
- A navigation bar at the bottom of the app controlling the ViewPager/PageView
Luckily Flutter provides a very useful skeleton for building this type of Layout: Scaffold
But first things first: A MaterialApp Widget should be the root of our Application to provide the material design we all love 👍
class LimeApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Lime',
home: new MainPage(),
);
}
}
LimeApp will be the entry point of our Application which we wont touch so far. So lets have a look how we should build our MainPage to look and feel like we want it to have.
Since we have to do some controlling inside the MainPage we will declare it as StatefulWidget
class MainPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _MainPageState();
}
}
Now we should talk about the _MainPageState. As written somewhere above: Please make sure to know what Widgets and their States are and how to basically build layouts using Flutter!
The App has to display three different pages to the user: trends, feed and community. We will use a PageView and place some Placeholders inside it for now.
class _MainPageState extends State<MainPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new PageView(
children: [
new Container(color: Colors.red),
new Container(color: Colors.blue),
new Container(color: Colors.grey)
]
)
);
}
}
We can replace the simple colored containers later with our more sophisticated widgets.
This is how the App should look like right know.
Creating the bottom navigation is amazingly simple using the Scaffold 😳 All we have to do is provide a BottomNavigationBar. Because there is no flame/fire icon in the standard icon set are we using something else for now. Don't worry getting different icons is pretty simple, but wont be covered in this article.
Here is how it looks like:
class _MainPageState extends State<MainPage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new PageView(
children: [
new Container(color: Colors.red),
new Container(color: Colors.blue),
new Container(color: Colors.grey)
]
),
bottomNavigationBar: new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.add),
title: new Text("trends")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.location_on),
title: new Text("feed")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.people),
title: new Text("community")
)
]
)
);
}
}
That was extremely simple, wasn't it?
But now we have to provide some controlling for the navigation 😰
What we have to do is using a PageController
class _MainPageState extends State<MainPage> {
/// This controller can be used to programmatically
/// set the current displayed page
PageController _pageController;
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new PageView(
children: [
...
],
/// Specify the page controller
controller: _pageController
),
bottomNavigationBar: new BottomNavigationBar(
items: [
...
],
/// Will be used to scroll to the next page
/// using the _pageController
onTap: navigationTapped,
)
);
}
/// Called when the user presses on of the
/// [BottomNavigationBarItem] with corresponding
/// page index
void navigationTapped(int page){
// Animating to the page.
// You can use whatever duration and curve you like
_pageController.animateToPage(
page,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
@override
void initState() {
super.initState();
_pageController = new PageController();
}
@override
void dispose(){
super.dispose();
_pageController.dispose();
}
}
As you can easily see controlling the PageView is simple and fun! 😋
Here is what we did:
- Create a PageController inside the .initState()
- Delegate the controller to the PageView as Param
- Handle the onTap event of BottomNavigationBar
- Animate to the page you want using a custom Duration and a custom Curve
- Call .dispose() on the PageController once the State gets disposed!
So we are facing one last problem: Updating the bottom navigation to indicate the correct page. Therefore a simple integer is introduced in the _MainPageState indicating which page is currently displayed.
class _MainPageState extends State<MainPage> {
/// This controller can be used to programmatically
/// set the current displayed page
PageController _pageController;
/// Indicating the current displayed page
/// 0: trends
/// 1: feed
/// 2: community
int _page = 0;
...
To implement this information in the view we have to simply update the BottomNavigationBar as the following
bottomNavigationBar: new BottomNavigationBar(
items: [
...
],
/// Will be used to scroll to the next page
/// using the _pageController
onTap: navigationTapped,
currentIndex: _page
)
Last step: Listen to the page changes and call .setState(). We will start by creating a new method called onPageChanged(int page) inside the _MainPageState
void onPageChanged(int page){
setState((){
this._page = page;
});
}
To make sure, that this method gets called, add it as callback to the PageView:
new PageView(
children: [
new Container(color: Colors.red),
new Container(color: Colors.blue),
new Container(color: Colors.grey)
],
/// Specify the page controller
controller: _pageController,
onPageChanged: onPageChanged
)
That's all: Basic Layout is done. Everything looks like we want it to be. 😎 Here is all we have so far:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(new LimeApp());
}
class LimeApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Lime',
home: new MainPage(),
);
}
}
class MainPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _MainPageState();
}
}
class _MainPageState extends State<MainPage> {
/// This controller can be used to programmatically
/// set the current displayed page
PageController _pageController;
/// Indicating the current displayed page
/// 0: trends
/// 1: feed
/// 2: community
int _page = 0;
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new PageView(
children: [
new Container(color: Colors.red),
new Container(color: Colors.blue),
new Container(color: Colors.grey)
],
/// Specify the page controller
controller: _pageController,
onPageChanged: onPageChanged
),
bottomNavigationBar: new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.add),
title: new Text("trends")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.location_on),
title: new Text("feed")
),
new BottomNavigationBarItem(
icon: new Icon(Icons.people),
title: new Text("community")
)
],
/// Will be used to scroll to the next page
/// using the _pageController
onTap: navigationTapped,
currentIndex: _page
)
);
}
/// Called when the user presses on of the
/// [BottomNavigationBarItem] with corresponding
/// page index
void navigationTapped(int page){
// Animating to the page.
// You can use whatever duration and curve you like
_pageController.animateToPage(
page,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
void onPageChanged(int page){
setState((){
this._page = page;
});
}
@override
void initState() {
super.initState();
_pageController = new PageController();
}
@override
void dispose(){
super.dispose();
_pageController.dispose();
}
}
Any of Lime's three pages (trends, feed and community) is displaying a almost endless scrolling list. The plan is to build one basic layout which handles data loading, pagination and refresh for us. It should use a generic interface or function to load specific data and one to adapt a Widget from given loaded data.
Here is how it looks like:
typedef Future<List<T>> PageRequest<T> (int page, int pageSize);
typedef Widget WidgetAdapter<T>(T t);
A given PageRequest takes page and pageSize as arguments, while page represents the page index (0 first page, 1 second Page, ...) and pageSize the exact count of items to load, and returns a List of generic items asynchronously.
Once our LoadingListView (that is how I called it) successfully loaded items using some kind of PageRequest, the WidgetAdapter is used to build Widgets from a generic item if needed.
Providing implementations of those two type definitions to our LoadingListView will give us the freedom to reuse the LoadingListView for almost anything needed in Lime ✔️
The LoadingListView should be pretty straight forward.
class LoadingListView<T> extends StatefulWidget {
/// Abstraction for loading the data.
/// This can be anything: An API-Call,
/// loading data from a certain file or database,
/// etc. It will deliver a list of objects (of type T)
final PageRequest<T> pageRequest;
/// Used for building Widgets out of
/// the fetched data
final WidgetAdapter<T> widgetAdapter;
/// The number of elements requested for each page
final int pageSize;
/// The number of "left over" elements in list which
/// will trigger loading the next page
final int pageThreshold;
/// [PageView.reverse]
final bool reverse;
final Indexer<T> indexer;
LoadingListView(this.pageRequest, {
this.pageSize: 50,
this.pageThreshold:10,
@required this.widgetAdapter,
this.reverse: false,
this.indexer
});
@override
State<StatefulWidget> createState() {
return new _LoadingListViewState();
}
}
But we know: Its all about the State! Obviously we need to hold some kind of reference to the fetched objects and we are going to display them using the standard ListView
class _LoadingListViewState<T> extends State<LoadingListView<T>> {
/// Contains all fetched elements ready to display!
List<T> objects = [];
@override
Widget build(BuildContext context) {
ListView listView = new ListView.builder(
itemBuilder: itemBuilder,
itemCount: objects.length,
reverse: widget.reverse
);
return listView;
}
}
Looks pretty nice so far, but how does itemBuilder look like and what does it do?
Widget itemBuilder(BuildContext context, int index) {
return widget.widgetAdapter != null ? widget.widgetAdapter(objects[index])
: new Container();
}
It basically builds the widgets from the fetched data! And i know: The null-check is unnecessary, who cares!
Don't forget to add your imports!
import 'dart:async';
I will now introduce two methods for the data-loading logic
- loadNext()
- lockedLoadNext() and you will see why i chose using two methods.
Future loadNext() async {
int page = (objects.length / widget.pageSize).floor();
List<T> fetched = await widget.pageRequest(page, widget.pageSize);
if(mounted) {
this.setState(() {
objects.addAll(fetched);
});
}
}
What happens there step by step?
- Step 1: figure out which page index should be loaded next
- Step 2: use the PageRequest provided by the Widget to load the data
- Step 3: add the fetched objects to the list and use .setState to notify the underlying ListView
So what do I need a second method for?
Calling the async method loadNext() will immediately return a Future object which runs the IO-Operation in the Background. We will have to introduce some kind of locking mechanism to prevent multiple requests running at the same time!
I decided to use the Future object returned by loadNext() as an indicator of any running background request.
class _LoadingListViewStateO<T> extends State<LoadingListView<T>> {
List<T> objects = [];
/// A Future returned by loadNext() if there
/// is currently a request running
/// or null, if no request is performed.
Future request;
And here the lockedLoadNext() which will take care of the newly introduced "request" reference
void lockedLoadNext() {
if (this.request == null) {
this.request = loadNext().then((x) {
this.request = null;
});
}
}
Again - step by step:
- Step 1: Check if there is any request currently running? Skip the entire method when this.request ist NOT NULL
- Step 2: Indicate that a request is currently running by assigning the Future returned by .loadNext() to request
- Step 3: Make sure to un-reference once the request has finished.
Since we now have built the data loading logic, we need to call it!
We will use .initState to perform trigger data loading for the first time:
@override
void initState() {
super.initState();
this.lockedLoadNext();
}
But how can we now when we have trigger loading the next page? One way, which worked for me, is abusing the itemBuilder introduced earlier, since we know that it will build Widgets for a certain index in the list.
Widget itemBuilder(BuildContext context, int index) {
/// here we go: Once we are entering the threshold zone, the loadLockedNext()
/// is triggered.
if (index + widget.pageThreshold > objects.length) {
loadLockedNext();
}
return widget.widgetAdapter != null ? widget.widgetAdapter(objects[index])
: new Container();
}
Data might change. We want to build some kind of swipe to refresh mechanism refetching the first page! Using flutters RefreshIndicator makes this task incredibly easy! ❤️
Just pass the ListView as child to the RefreshIndicator inside the build method:
@override
Widget build(BuildContext context) {
ListView listView = new ListView.builder(
itemBuilder: itemBuilder,
itemCount: objects.length,
reverse: widget.reverse
);
return new RefreshIndicator(
onRefresh: onRefresh,
child: listView
);
}
Last thing to do: Implement onRefresh
Future onRefresh() async {
this.request?.timeout(const Duration());
List<T> fetched = await widget.pageRequest(0, widget.pageSize);
setState(() {
this.objects = fetched;
});
return true;
}
Step by step:
- Step 1: Cancel any currently running request
- Step 2: Use pageRequest directly to fetch the first page
- Step 3: Set and display the fetched data