Flutter News App UI - Flutter UI | Devhubspot
Today, we will build a Flutter News App UI. With this Flutter tutorial for beginners, you will learn how to build the UI of a news app from scratch using Flutter 3.
Create a Flutter App
If everything is properly set up, then in order to create a project we can simply run the following command in our desired local directory:
flutter create newsapp
After we've set up the project, we can navigate inside the project directory and execute the following command in the terminal to run the project in either an available emulator or an actual device:
flutter run
First, we need to make some simple configurations to the default boilerplate code in the main.dart file. We'll remove some default code and add following code
import 'package:flutter/material.dart';
import 'package:newsapp/screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter News App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.grey,
useMaterial3: true,
),
home: HomeScreen(),
);
}
}
import 'package:newsapp/screens/home_screen.dart'; void main() {
runApp(const MyApp());
} class MyApp extends StatelessWidget {
const MyApp({super.key}); // This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter News App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.grey,
useMaterial3: true,
),
home: HomeScreen(),
);
}
}
Before Create bottom_nav_bar.dart,
custom_tag,dart and
image_container.dart
stateful widget file in widget folder, then inside of it with the following code:
import 'package:flutter/material.dart'; import 'package:newsapp/screens/home_screen.dart'; import 'package:newsapp/screens/search_screen.dart'; class BottomNavBar extends StatelessWidget { final int index; const BottomNavBar({super.key, required this.index}); @override Widget build(BuildContext context) { return BottomNavigationBar( currentIndex: index, showSelectedLabels: false, showUnselectedLabels: false, selectedItemColor: Colors.black, unselectedItemColor: Colors.black.withAlpha(100), items: [ BottomNavigationBarItem( icon: Container( margin: EdgeInsets.only(left: 50), child: IconButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => HomeScreen())); }, icon: Icon(Icons.home)), ), label: "Home"), BottomNavigationBarItem( icon: Container( // margin: EdgeInsets.only(left: 50), child: IconButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => SearchScreen())); }, icon: Icon(Icons.search)), ), label: "Search"), BottomNavigationBarItem( icon: Container( margin: EdgeInsets.only(right: 50), child: IconButton( onPressed: () { // Navigator.of(context).push( // MaterialPageRoute(builder: (context) => HomeScreen())); }, icon: Icon(Icons.person)), ), label: "Profile") ], ); } }
custom_tag.dart
import 'package:flutter/material.dart';
class CustomTag extends StatelessWidget {
final Color backgroundColor;
final List<Widget> children;
const CustomTag(
{super.key, required this.backgroundColor, required this.children});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}
}
image_container.dart
import 'package:flutter/material.dart';
class ImageContainer extends StatelessWidget {
final double width;
final double? height;
final String imageUrl;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? borderRadius;
final Widget? child;
const ImageContainer(
{super.key,
required this.width,
this.height,
required this.imageUrl,
this.padding,
this.margin,
this.borderRadius,
this.child});
@override
Widget build(BuildContext context) {
return Container(
height: height,
width: width,
margin: margin,
padding: padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
image: DecorationImage(
image: NetworkImage(imageUrl), fit: BoxFit.cover)),
child: child,
);
}
}
After Create home_screen.dart
stateful widget file in screens folder like screens/home_screen.dart, then inside of it with the following code:
import 'package:flutter/material.dart';
import 'package:newsapp/models/article_model.dart';
import 'package:newsapp/screens/detail_screen.dart';
import 'package:newsapp/widgets/bottom_nav_bar.dart';
import 'package:newsapp/widgets/custom_tag.dart';
import 'package:newsapp/widgets/image_container.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Article article = Article.articles[0];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
onPressed: () {},
icon: Icon(
Icons.menu,
color: Colors.white,
),
),
),
bottomNavigationBar: BottomNavBar(index: 0),
extendBodyBehindAppBar: true,
body: ListView(
padding: EdgeInsets.zero,
children: [
NewsOfTheDay(article: article),
PopularNews(articles: Article.articles),
BreakingNews(articles: Article.articles),
],
),
);
}
}
class BreakingNews extends StatelessWidget {
final List<Article> articles;
const BreakingNews({super.key, required this.articles});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Breaking News",
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontWeight: FontWeight.bold),
),
Text("More", style: Theme.of(context).textTheme.bodyLarge)
],
),
SizedBox(
height: 20,
),
SizedBox(
height: 230,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: articles.length,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.of(context).size.width * 0.5,
margin: EdgeInsets.only(right: 10),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailScreen(articles[index])));
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ImageContainer(
width: MediaQuery.of(context).size.width * 0.5,
height: 120,
imageUrl: articles[index].imageUrl),
SizedBox(height: 10),
Text(
articles[index].title,
maxLines: 2,
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(
fontWeight: FontWeight.bold, height: 1.5),
),
SizedBox(height: 5),
Text(
'${DateTime.now().difference(articles[index].createdAt).inHours}',
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall),
SizedBox(height: 5),
Text('by ${articles[index].author}',
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
},
),
)
],
),
);
}
}
class PopularNews extends StatelessWidget {
final List<Article> articles;
const PopularNews({super.key, required this.articles});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Popular News",
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontWeight: FontWeight.bold),
),
Text("More", style: Theme.of(context).textTheme.bodyLarge)
],
),
SizedBox(
height: 20,
),
SizedBox(
height: 230,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: articles.length,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.of(context).size.width * 0.5,
margin: EdgeInsets.only(right: 10),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailScreen(articles[index])));
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ImageContainer(
width: MediaQuery.of(context).size.width * 0.5,
height: 120,
imageUrl: articles[index].imageUrl),
SizedBox(height: 10),
Text(
articles[index].title,
maxLines: 2,
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(
fontWeight: FontWeight.bold, height: 1.5),
),
SizedBox(height: 5),
Text(
'${DateTime.now().difference(articles[index].createdAt).inHours}',
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall),
SizedBox(height: 5),
Text('by ${articles[index].author}',
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
},
),
)
],
),
);
}
}
class NewsOfTheDay extends StatelessWidget {
final Article article;
const NewsOfTheDay({super.key, required this.article});
@override
Widget build(BuildContext context) {
return ImageContainer(
height: MediaQuery.of(context).size.height * 0.45,
width: double.infinity,
padding: const EdgeInsets.all(20.0),
imageUrl: article.imageUrl,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTag(backgroundColor: Colors.grey.withAlpha(150), children: [
Text(
"News of the Day",
style: Theme.of(context).textTheme.headlineSmall!.copyWith(
fontWeight: FontWeight.bold,
height: 1.25,
color: Colors.white),
)
]),
SizedBox(
height: 10,
),
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall!.copyWith(
fontWeight: FontWeight.bold,
height: 1.25,
color: Colors.white),
),
TextButton(
onPressed: () {},
child: Row(
children: [
Text(
"Learn More",
style: Theme.of(context)
.textTheme
.headlineLarge!
.copyWith(color: Colors.white),
),
SizedBox(
width: 10,
),
Icon(
Icons.arrow_right_alt,
color: Colors.white,
)
],
))
],
));
}
}
After Create detail_screen.dart
stateful widget file in screens folder like screens/detail_screen.dart, then inside of it with the following code:
import 'package:flutter/material.dart';
import 'package:newsapp/models/article_model.dart';
import 'package:newsapp/widgets/custom_tag.dart';
import 'package:newsapp/widgets/image_container.dart';
class DetailScreen extends StatefulWidget {
const DetailScreen(Article articl, {super.key});
@override
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
Article article = Article.articles[1];
@override
Widget build(BuildContext context) {
return ImageContainer(
width: double.infinity,
imageUrl: article.imageUrl,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
iconTheme: IconThemeData(color: Colors.white),
backgroundColor: Colors.transparent,
elevation: 0,
),
extendBodyBehindAppBar: true,
body: ListView(
children: [
NewsHeadline(article: article),
NewsBody(article: article),
],
),
),
);
}
}
class NewsBody extends StatelessWidget {
final Article article;
const NewsBody({super.key, required this.article});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)),
color: Colors.white),
child: Column(
children: [
Row(
children: [
CustomTag(
backgroundColor: Colors.black,
children: [
CircleAvatar(
radius: 10,
backgroundImage: NetworkImage(article.authorImageUrl),
),
SizedBox(width: 10),
Text(
article.author,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.white),
)
],
),
SizedBox(width: 10),
CustomTag(
backgroundColor: Colors.grey.shade200,
children: [
Icon(Icons.timer, color: Colors.grey),
SizedBox(width: 10),
Text(
'${DateTime.now().difference(article.createdAt).inHours}h',
style: Theme.of(context).textTheme.bodyMedium,
)
],
),
SizedBox(width: 10),
CustomTag(backgroundColor: Colors.grey.shade200, children: [
Icon(Icons.remove_red_eye, color: Colors.grey),
SizedBox(width: 10),
Text(
'${article.views}',
style: Theme.of(context).textTheme.bodyMedium,
)
])
],
),
SizedBox(height: 20),
Text(
article.title,
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Text(
article.body,
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(height: 1.5),
),
SizedBox(height: 20),
GridView.builder(
shrinkWrap: true,
itemCount: 2,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, childAspectRatio: 1.25),
itemBuilder: (context, index) {
return ImageContainer(
width: MediaQuery.of(context).size.width * 0.42,
imageUrl: article.imageUrl,
margin: EdgeInsets.only(right: 5.0, bottom: 5.0),
);
},
)
],
),
);
}
}
class NewsHeadline extends StatelessWidget {
final Article article;
const NewsHeadline({super.key, required this.article});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.15,
),
CustomTag(backgroundColor: Colors.grey.withAlpha(150), children: [
Text(
article.category,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.white),
)
]),
SizedBox(height: 10),
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall!.copyWith(
fontWeight: FontWeight.bold, color: Colors.white, height: 1.25),
),
SizedBox(height: 10),
Text(
article.subtitle,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Colors.white,
),
),
],
),
);
}
}
After Create search_screen.dart
stateful widget file in screens folder like screens/search_screen.dart, then inside of it with the following code:
import 'package:flutter/material.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
Create a Model folder in lib and create a file for demo data
article_model.dart
class Article { final String id; final String title; final String subtitle; final String body; final String author; final String authorImageUrl; final String category; final String imageUrl; final int views; final DateTime createdAt; const Article({ required this.id, required this.title, required this.subtitle, required this.body, required this.author, required this.authorImageUrl, required this.category, required this.imageUrl, required this.views, required this.createdAt, }); static List<Article> articles = [ Article( id: '1', title: 'Lorem ipsum dolor sit amet, consectetur elit. Cras molestie maximus', subtitle: 'Aliquam laoreet ante non diam suscipit accumsan. Sed vel consequat leo, non suscipit odio. Aliquam turpis', body: 'Nullam sed augue a turpis bibendum cursus. Suspendisse potenti. Praesent mi ligula, mollis quis elit ac, eleifend vestibulum ex. Nullam quis sodales tellus. Integer feugiat dolor et nisi semper luctus. Nulla egestas nec augue facilisis pharetra. Sed ultricies nibh a odio aliquam, eu imperdiet purus aliquam. Donec id ante nec', author: 'Anna G. Wright', authorImageUrl: 'https://images.unsplash.com/photo-1658786403875-ef4086b78196?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80', category: 'Politics', views: 1204, imageUrl: 'https://images.unsplash.com/photo-1656106534627-0fef76c8b042?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80', createdAt: DateTime.now().subtract(const Duration(hours: 5)), ), Article( id: '2', title: 'Sed sed molestie libero, et massa. Donec auctor vestibulum pellentesque', subtitle: 'Aliquam laoreet ante non diam suscipit accumsan. Sed vel consequat leo, non suscipit odio. Aliquam turpis', body: 'Nullam sed augue a turpis bibendum cursus. Suspendisse potenti. Praesent mi ligula, mollis quis elit ac, eleifend vestibulum ex. Nullam quis sodales tellus. Integer feugiat dolor et nisi semper luctus. Nulla egestas nec augue facilisis pharetra. Sed ultricies nibh a odio aliquam, eu imperdiet purus aliquam. Donec id ante nec', author: 'Anna G. Wright', authorImageUrl: 'https://images.unsplash.com/photo-1658786403875-ef4086b78196?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80', category: 'Politics', views: 1204, imageUrl: 'https://images.unsplash.com/photo-1574280363402-2f672940b871?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80', createdAt: DateTime.now().subtract(const Duration(hours: 6)), ), Article( id: '3', title: 'Aliquam ullamcorper ipsum, vel consequat sem finibus a. Donec lobortis', subtitle: 'Aliquam laoreet ante non diam suscipit accumsan. Sed vel consequat leo, non suscipit odio. Aliquam turpis', body: 'Nullam sed augue a turpis bibendum cursus. Suspendisse potenti. Praesent mi ligula, mollis quis elit ac, eleifend vestibulum ex. Nullam quis sodales tellus. Integer feugiat dolor et nisi semper luctus. Nulla egestas nec augue facilisis pharetra. Sed ultricies nibh a odio aliquam, eu imperdiet purus aliquam. Donec id ante nec', author: 'Anna G. Wright', authorImageUrl: 'https://images.unsplash.com/photo-1658786403875-ef4086b78196?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80', category: 'Politics', views: 1204, imageUrl: 'https://images.unsplash.com/photo-1616832880334-b1004d9808da?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1336&q=80', createdAt: DateTime.now().subtract(const Duration(hours: 8)), ), Article( id: '4', title: 'Proin mattis nec lorem at rutrum. Curabitur sit augue vel', subtitle: 'Aliquam laoreet ante non diam suscipit accumsan. Sed vel consequat leo, non suscipit odio. Aliquam turpis', body: 'Nullam sed augue a turpis bibendum cursus. Suspendisse potenti. Praesent mi ligula, mollis quis elit ac, eleifend vestibulum ex. Nullam quis sodales tellus. Integer feugiat dolor et nisi semper luctus. Nulla egestas nec augue facilisis pharetra. Sed ultricies nibh a odio aliquam, eu imperdiet purus aliquam. Donec id ante nec', author: 'Anna G. Wright', authorImageUrl: 'https://images.unsplash.com/photo-1658786403875-ef4086b78196?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80', category: 'Politics', views: 1204, imageUrl: 'https://images.unsplash.com/photo-1653587416464-8a99cc74d192?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=974&q=80', createdAt: DateTime.now().subtract(const Duration(hours: 19)), ), Article( id: '5', title: 'Donec lobortis lectus a iaculis rutrum. Vestibulum libero sit amet', subtitle: 'Aliquam laoreet ante non diam suscipit accumsan. Sed vel consequat leo, non suscipit odio. Aliquam turpis', body: 'Nullam sed augue a turpis bibendum cursus. Suspendisse potenti. Praesent mi ligula, mollis quis elit ac, eleifend vestibulum ex. Nullam quis sodales tellus. Integer feugiat dolor et nisi semper luctus. Nulla egestas nec augue facilisis pharetra. Sed ultricies nibh a odio aliquam, eu imperdiet purus aliquam. Donec id ante nec', author: 'Anna G. Wright', authorImageUrl: 'https://images.unsplash.com/photo-1658786403875-ef4086b78196?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80', category: 'Politics', views: 1204, imageUrl: 'https://images.unsplash.com/photo-1658330056737-0fd4bda0e4c1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1351&q=80', createdAt: DateTime.now().subtract(const Duration(hours: 20)), ), ]; @override List<Object?> get props => [ id, title, subtitle, body, author, authorImageUrl, category, imageUrl, createdAt, ]; }