Skip to content

Latest commit

 

History

History
698 lines (558 loc) · 21.3 KB

File metadata and controls

698 lines (558 loc) · 21.3 KB

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 **

What I wanted to build and why I tried Flutter

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:



GIF: Lime native Android App





Screenshot: Lime native Android App



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 ☺️. Since I am a great fan of Dart as a programming language, I found Flutter. And hell: I remember seeing the first code example of a layout and immediately leaving the website shaking my head. I visited all the other, well known, Frameworks like Xamarin, ReactNative or NativeScript and those are all great Frameworks but nothing could catch me entirely. At the end I cloned Flutter, gave it a chance by reading the docs and tried building my first layouts and quickly fell in love with it ❤️ But why did I try Flutter when my first impression wasn't that great at all?

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.

GIF: Lime Flutter version





Screenshot: Lime Flutter version



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 ❤️

What we need to do

Step 1: Build the basic layout

We will rebuild the basic layout of lime including the ViewPager and the bottom navigation.

Step 2: Build a list which supports pagination for data loading

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 ☺️

Step 3: Build the layout of a post

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.

Step 4: Build fading images

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

Step 5: Integrate first interaction: Make a post "likeable" and animate the action.

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.

Step 6: Profit. Didn't figure that one out, yet 😅

Step 1: Build the basic layout

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!

1.1 Creating a PageView with three children

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.

Screenshot: Building the PageView



1.2 Creating the bottom navigation

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")
          )
        ]
      )
    );
  }
}

Screenshot: Building the bottom navigation



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:



Resume: Basic layout so far, so good ☺️

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();
  }


}

GIF: Basic layout



Step 2: Build a list which supports pagination for data loading

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);

PageRequest

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.

WidgetAdapter

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 ✔️

So lets build it! ‼️

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';

Loading data

I will now introduce two methods for the data-loading logic

  • loadNext()
  • lockedLoadNext() and you will see why i chose using two methods.
loadNext()
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
lockedLoadNext()

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.
Initial loading and pagination

Since we now have built the data loading logic, we need to call it!

Initial loading

We will use .initState to perform trigger data loading for the first time:

  @override
  void initState() {
    super.initState();
    this.lockedLoadNext();
  }
Pagination

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();
  }
Implement a refresh mechanism

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