Flutter Challenge: The Medium App

 

At the very top, the article has a text which gives you relevance as to why it’s there. For example, if it’s based on your reading history or if it’s from a category you follow.

Below that is the title of the article itself, which is bold and black as to stand out.

Below the title is the author of the article, date of publishing, and how long the article is estimated to take to read.

On the right hand side is the article image and a bookmark button for saving it for later.

Setting up the app

Create a new Flutter, example: “medium_app_ui”.

Remove the code for the counter app until you only remain with a Scaffold with an AppBar.

 

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Home',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {

  @override
  _MyHomePageState createState() => new _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Home"),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
          ],
        ),
      ),
    );
  }
}

This is the app with a simple app bar.

Before we move on to the list, let’s construct the AppBar according to the Medium design.

As of now, the appBar is blue. We need to change the app bar to black. The actions on the appBar are notifications and search. Doing these changes:


appBar: <strong class="markup--strong markup--pre-strong">new </strong>AppBar(
  title: <strong class="markup--strong markup--pre-strong">new </strong>Text(<strong class="markup--strong markup--pre-strong">"Home"</strong>),
  backgroundColor: Colors.<em class="markup--em markup--pre-em">black</em>,
  actions: <Widget>[
    Icon(Icons.<em class="markup--em markup--pre-em">notifications_none</em>, color: Colors.<em class="markup--em markup--pre-em">grey</em>,),
    Icon(Icons.<em class="markup--em markup--pre-em">search</em>, color: Colors.<em class="markup--em markup--pre-em">grey</em>),
  ],
),</pre>


After this, we need to adjust a few icons and add a drawer. To add a drawer without any content, simply add

</pre><pre name="877c" class="graf graf--pre graf-after--p">drawer: Drawer(),

 

to your Scaffold.

Here is the code and resulting AppBar.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Home',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}

class MyHomePage extends StatefulWidget {

@override
_MyHomePageState createState() => new _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage> {

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Home", style: TextStyle(fontWeight: FontWeight.w400),),
backgroundColor: Colors.black,
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: Icon(Icons.notifications_none, color: Colors.grey,),
),
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Icon(Icons.search, color: Colors.grey),
),
],
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
],
),
),
drawer: Drawer(),
);
}
}

</pre>
<pre id="13bc" name="13bc" class="graf graf--pre graf-after--p"><strong class="markup--strong markup--pre-strong">class </strong>NewsArticle {

  String <strong class="markup--strong markup--pre-strong">categoryTitle</strong>;
  String <strong class="markup--strong markup--pre-strong">title</strong>;
  String <strong class="markup--strong markup--pre-strong">author</strong>;
  String <strong class="markup--strong markup--pre-strong">date</strong>;
  String <strong class="markup--strong markup--pre-strong">readTime</strong>;
  String <strong class="markup--strong markup--pre-strong">imageAssetName</strong>;

  NewsArticle(<strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">categoryTitle</strong>, <strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">title</strong>, <strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">author</strong>, <strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">date</strong>,<strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">readTime</strong>, <strong class="markup--strong markup--pre-strong">this</strong>.<strong class="markup--strong markup--pre-strong">imageAssetName</strong>);

}</pre>
<pre name="13bc" class="graf graf--pre graf-after--p">

For now, they’re just hardcoded articles.

Creating the List

To create a repeating list in Flutter, we use ListView.builder().

In the image, the cards are over a grey background.

Let’s analyse the card components themselves.

 

There’s a class named NewsHelper which provides the hard-coded articles.

The final item is build like this:

<pre id="f7fc" name="f7fc" class="graf graf--pre graf-after--p">Padding(
  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.fromLTRB(0.0,0.5,0.0,0.5),
  child: Card(
    child: Padding(
      padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.<strong class="markup--strong markup--pre-strong">start</strong>,
        children: <Widget>[
          Text(article.<strong class="markup--strong markup--pre-strong">categoryTitle</strong>, style: TextStyle(color: Colors.<em class="markup--em markup--pre-em">black38</em>,fontWeight: FontWeight.<em class="markup--em markup--pre-em">w500</em>, fontSize: 16.0),),
          Padding(
            padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.fromLTRB(0.0,12.0,0.0,12.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.<strong class="markup--strong markup--pre-strong">spaceBetween</strong>,
              children: <Widget>[
                Flexible(child: Text(article.<strong class="markup--strong markup--pre-strong">title</strong>, style: TextStyle(fontWeight: FontWeight.<em class="markup--em markup--pre-em">bold</em>, fontSize: 22.0),), flex: 3,),
                Flexible(
                  flex: 1,
                  child: Container(
                    height: 80.0,
                      width: 80.0,
                      child: Image.asset(<strong class="markup--strong markup--pre-strong">"assets/" </strong>+ article.<strong class="markup--strong markup--pre-strong">imageAssetName</strong>, fit: BoxFit.<strong class="markup--strong markup--pre-strong">cover</strong>,)
                  ),
                ),
              ],
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.<strong class="markup--strong markup--pre-strong">spaceBetween</strong>,
            children: <Widget>[
              Column(
                crossAxisAlignment: CrossAxisAlignment.<strong class="markup--strong markup--pre-strong">start</strong>,
                children: <Widget>[
                  Text(article.<strong class="markup--strong markup--pre-strong">author</strong>, style: TextStyle(fontSize: 18.0),),
                  Text(article.<strong class="markup--strong markup--pre-strong">date </strong>+ <strong class="markup--strong markup--pre-strong">" . " </strong>+ article.<strong class="markup--strong markup--pre-strong">readTime</strong>, style: TextStyle(color: Colors.<em class="markup--em markup--pre-em">black45</em>, fontWeight: FontWeight.<em class="markup--em markup--pre-em">w500</em>),)
                ],
              ),
              Icon(Icons.<em class="markup--em markup--pre-em">bookmark_border</em>),
            ],
          )
        ],
      ),
    ),
  ),
);</pre>

Designing the Drawer

This is how the drawer looks on the Medium app:

The Drawer is simply a Column with a list of elements and an Image at the top. The top white portion can be represented by one container and the bottom half by another.

Here is the drawer in code:

<pre id="671f" name="671f" class="graf graf--pre graf-after--p">Drawer(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.<strong class="markup--strong markup--pre-strong">start</strong>,
    children: <Widget>[
      Container(
        child: Padding(
          padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.fromLTRB(32.0,64.0,32.0,16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.<strong class="markup--strong markup--pre-strong">start</strong>,
            children: <Widget>[
              Icon(Icons.<em class="markup--em markup--pre-em">account_circle</em>, size: 90.0,),
              Padding(
                padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                child: Text(<strong class="markup--strong markup--pre-strong">"John Doe"</strong>, style: TextStyle(fontSize: 20.0),),
              ),
              Padding(
                padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                child: Text(<strong class="markup--strong markup--pre-strong">"See profile"</strong>, style: TextStyle(color: Colors.<em class="markup--em markup--pre-em">black45</em>),),
              )
            ],
          ),
        ),
      ),
      Expanded(
        child: Container(
          color: Colors.<em class="markup--em markup--pre-em">black12</em>,
          child: Padding(
            padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.fromLTRB(40.0,16.0,40.0,40.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.<strong class="markup--strong markup--pre-strong">start</strong>,
              children: <Widget>[
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Home"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Audio"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Bookmarks"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Interests"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Divider(),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Become a member"</strong>, style: TextStyle(fontSize: 18.0, color: Colors.<em class="markup--em markup--pre-em">teal</em>),),
                ),
                Divider(),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"New Story"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Stats"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
                Padding(
                  padding: <strong class="markup--strong markup--pre-strong">const </strong>EdgeInsets.all(8.0),
                  child: Text(<strong class="markup--strong markup--pre-strong">"Drafts"</strong>, style: TextStyle(fontSize: 18.0),),
                ),
              ],
            ),
          ),
        ),
      ),
    ],
  ),
),</pre>

Here is the final code of the page:


import 'package:flutter/material.dart';
import 'package:medium_app_ui/NewsArticle.dart';
import 'package:medium_app_ui/NewsHelper.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Home',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}

class MyHomePage extends StatefulWidget {

@override
_MyHomePageState createState() => new _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage> {

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Home", style: TextStyle(fontWeight: FontWeight.w400),),
backgroundColor: Colors.black,
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Icon(Icons.notifications_none, color: Colors.grey,),
),
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Icon(Icons.search, color: Colors.grey),
),
],
),
body: ListView.builder(
itemBuilder: (context, position) {

NewsArticle article = NewsHelper.getArticle(position);

return Padding(
padding: const EdgeInsets.fromLTRB(0.0,0.5,0.0,0.5),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(article.categoryTitle, style: TextStyle(color: Colors.black38,fontWeight: FontWeight.w500, fontSize: 16.0),),
Padding(
padding: const EdgeInsets.fromLTRB(0.0,12.0,0.0,12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: Text(article.title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22.0),), flex: 3,),
Flexible(
flex: 1,
child: Container(
height: 80.0,
width: 80.0,
child: Image.asset("assets/" + article.imageAssetName, fit: BoxFit.cover,)
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(article.author, style: TextStyle(fontSize: 18.0),),
Text(article.date + " . " + article.readTime, style: TextStyle(color: Colors.black45, fontWeight: FontWeight.w500),)
],
),
Icon(Icons.bookmark_border),
],
)
],
),
),
),
);
},
itemCount: NewsHelper.articleCount,
),
drawer: Drawer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(32.0,64.0,32.0,16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(Icons.account_circle, size: 90.0,),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("John Doe", style: TextStyle(fontSize: 20.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("See profile", style: TextStyle(color: Colors.black45),),
)
],
),
),
),
Expanded(
child: Container(
color: Colors.black12,
child: Padding(
padding: const EdgeInsets.fromLTRB(40.0,16.0,40.0,40.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Home", style: TextStyle(fontSize: 18.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Audio", style: TextStyle(fontSize: 18.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Bookmarks", style: TextStyle(fontSize: 18.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Interests", style: TextStyle(fontSize: 18.0),),
),
Divider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Become a member", style: TextStyle(fontSize: 18.0, color: Colors.teal),),
),
Divider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("New Story", style: TextStyle(fontSize: 18.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Stats", style: TextStyle(fontSize: 18.0),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Drafts", style: TextStyle(fontSize: 18.0),),
),
],
),
),
),
),
],
),
),
);
}
}

That’s it for today. Feel free to suggest any other apps you may want to see recreated in Flutter.

Project GitHub link : https://github.com/deven98/MediumAppFlutter