Intro
Hey all, Welcome back! Yup, flutter this time, God...am just amazing! ๐ just kidding... I've worked on flutter for a while, so, don't worry I will explain it properly, have faith guys.
I know the previous blog was a bit longer, but no worries, this time it's gonna be very short and very easy to catch on to.
Prerequisites
Installation:
Android Studio/ VScode + emulator installed with flutter plugin on the system.
Flutter SDK installed
JVM installed & JAVA_HOME set to the system
Install the flutter plugin in your code editor.
Concepts:
Basics of Flutter & Dart
Basic understanding of stream controller & stream builder.
Widgets life cycle & FadeInImage() widget
The installation process is mandatory and can be done with the help of official flutter documentation. Also, familiarity with the basic concepts is beneficial to have a bit of edge while implementing but it's not mandatory as I will be explaining the required concepts on the go.
Project Overview:
The final goal of our short project is to implement a simple clock on the screen with a gif of a clock &a dark background as shown in the image below.
As simple as that...
Setting Up the Environment
To create a new flutter project on VScode click on the blog link & go through it.
To create a new flutter project on Android Studio click on the blog link & go through it.
Once created the project, open the emulator and run the app (basic boiler-plate code) to see the output.
Working on the Project
Now that we've set up the environment, it's time to move toward development. First of all, create a new dart file named home_page.dart as shown below,
Adding assets for further use
Create an assets folder inside the project directory with a sub-directory named images. We're going to need a clock image to add to the project, for which you'll need to download an image and save it to the assets/images directory as shown below.
After that open the pubspec.yaml file, remove all the commented data, & then add the below code at the end with proper indentation. Indentation is very important for the pubspec.yaml file so, be careful while doing that.
assets:
- assets/images/
The indentation should be as given in the image below,
Once you are done with the above process, open the terminal of the code editor & run the command flutter pub get to configure the assets, if you get an error while running the command check the indentation & run again.
Configuring main.dart
After that open the main.dart file with a double click of the mouse & removes all the boiler-plate code.
Once you are done with it, add the code given below inside the main.dart file,
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
Initially, you will get an error on HomePage(), but ignore it for now as we're going to cover it in the next step itself. Also, ensure you've imported the home_page.dart in the main.dart i.e. ensure you've added import 'home_page.dart'; in main.dart file.
A quick explanation of the above code:
When we run the flutter application, the main() method gets triggered which runs runApp(const MyApp()) as a main thread which then executes the build method of MyApp() to implement the UI on the screen. At the home in MyApp() we've given a stateful widget HomePage() which we will write in the home_page.dart to execute our main code for the project.
Finally, your main.dart should look similar to the image added below,
Moving towards home_page.dart
Open the home_page.dart & just copy-paste the initial basic code given below, so that we could start building the actual code logic...
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Hii'),),
);
}
}
If you run the app now, you will get Hii printed on the emulator screen in the center.
Focusing on Clock
To implement a clock, there's a DateTime.now() instance which gives the instantaneous date & time, but to print that time at every second we will need a function that will be called after each second & return the date-time data.
If we summarise the above statement in short, we could say that we just need a repetitive function call at the period of 1 second, & what better way than using streams for that? Let's understand them one by one,
Stream Controller & stream:
In a nutshell, stream controllers in flutter control the data flow in the stream, i.e. whenever a new data sink (add) inside the stream controller, the receiver i.e. the point where we are listening to the stream, will get notified & updated with the new data.
We will be using the stream controller for the same purpose, i.e. we will sink in (add) the date-time of the current instance in the stream after every second, once new data get in we will listen to it at the receiving end.
To initialize the stream controller, use the code final StreamController _streamController = StreamController(); at the exact position specified below,
class _HomePageState extends State<HomePage> { final StreamController _streamController = StreamController(); @override Widget build(BuildContext context) { return Scaffold( body: Center(child: Text('Hii'),), ); } }
Time to Write the realTimeClock() function:
Inside an asynchronous realTimeClock() function, we will be adding the date-time of the current instance inside the stream.
The DateTime.now() function gives us the date-time as, 2023-02-09 15:45:16.018146, in which 2023-02-09 is the date in yyyy-mm-dd format & 15:45:16.018146 is the time in 24-hour.
We will use,
a. var current = DateTime.now().toString().split(' '); to separate the date and time in a list.
b. var date = List.from(current[0].split('-').reversed).join('/'); to separate the date, converting it to a list like [yyyy, mm, dd], reversing the list & joining it with / to get the date in the dd/mm/yyyy format.
c. var time = current[1]; to get the time that we stored in current at index 1, we will format the time at the time of execution as there's not much to do.
Now that we've formatted the data in the required format, it's time to add this data inside the _streamController, keep in mind to add this function just below our controller as shown below,
class _HomePageState extends State<HomePage> { final StreamController _streamController = StreamController(); Future<void> realTimeClock() async{ var current = DateTime.now().toString().split(' '); var date = List.from(current[0].split('-').reversed).join('/'); var time = current[1]; var data = { 'date' : date, 'time' : time, }; _streamController.sink.add(data); } @override Widget build(BuildContext context) { return Scaffold( body: Center(child: Text('Hii'),), ); } }
Repeating Function Calls
There are several ways in which we can achieve a repeating function call, it doesn't matter what way we choose, the important thing is where & when are we going to trigger this function call. Let's talk about this a bit,
So, when the home_page.dart opens on the screen, we expect it should show the clock right? yup, & to achieve that we will need to initiate/trigger the repeating function calls before the build function starts executing... & for the same thing we have something called the initState() in flutter. If you've gone through the widget life cycle then you might be aware that, this initState() executes only once & before calling the build function, exactly what we want.
Now, what we will do is, we will use Timer to execute repeating calls with Duration of 1 second & we will trigger the function in the initState(), as we've discussed before. Write this code just after the realTimeClock() function.
@override void initState() { // TODO: implement initState super.initState(); Timer.periodic(const Duration(seconds: 1), (timer) { realTimeClock(); }); }
Cool, now we've to add a dispose() just below the initState() to dispose the stream.
@override void dispose() { // TODO: implement dispose super.dispose(); _streamController.close(); }
Hurray! why? well, if you haven't realized yet... we're completed with most of the logical part & the only thing remaining is to use the result of this logic in the UI...that's it & we are done, let's do it then,
Now, from here onwards we will be working only inside the Scaffold(), so remember to write the code inside it only, until we complete the UI.
Creating the UI
Why StreamBuilder:
Okay, as we are using streams, so, it's quite obvious to choose the StreamBuilder, the name itself says that it helps in building the streams. How it helps that's something we need to understand, so, the stream we created earlier, needs a listener at the receiving end so that when the data gets updated the listener should listen to the update i.e. whenever the data updates in the stream we need a StreamBuilder to listen to it each time.
Also, StreamBuilder handles conditions like if the stream has data, has an error & if there's no data at all.
As we can see, we've provided the stream to the StreamBuilder as
stream: _streamController.stream, along with I've handled the .hasError() by printing the error on screen for now, & the no data condition in the else part by showing a circular progress indicator.
@override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: StreamBuilder( stream: _streamController.stream, builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) { if (snapshot.hasData) { //When updated data gets in return Text('Data prints here'); } else if (snapshot.hasError) { //If some error while calling the stream return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Center(child: Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.white),),), ); } else { //If there's no data, i.e. data yet to be fetched return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: const Center(child: CircularProgressIndicator()), ); } } ) ); }
StreamBuilder .hasData:
The only thing remaining is to handle the case where the StreamBuilder has gotten the data from the stream. As we have to add an image/gif of the clock following the date-time we are getting from the stream, we will be using the Column() widget, & wrapped it with the Center() widget to take everything in the center of the screen. I've used MediaQuery to ensure responsiveness wherever seemed necessary.
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) { if (snapshot.hasData){ var data = snapshot.data; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FadeInImage( fit: BoxFit.fill, height: MediaQuery.of(context).size.height/2, image: const NetworkImage('https://media.giphy.com/media/j0qlQDHk2HTVuwImpo/giphy.gif'), placeholder: AssetImage('assets/images/images.png'), imageErrorBuilder: (context, error, stackTrace){ return Image.asset( 'assets/images/images.png', fit: BoxFit.fill, ); }), Container( width: MediaQuery.of(context).size.width /1.2, child: Text( '${data['date']} | ${data['time'].substring(0,data['time'].indexOf('.'))}', style: const TextStyle( fontSize: 34, color: Colors.white, ), ), ) ], ), ); } }
In the above code snippet FadeInImage() handles three conditions,
To show the NetworkImage() from the internet
Add a placeholder AssetImage() from the directory until the internet image loads.
To handle the error by adding the same image from the system with the help of Image.asset() widget inside imageErrorBuilder.
Other parameters are there to manage the dimensions of the image.
Now, I've saved the snapshot.data in a variable as,
var data = snapshot.data; for simplicity.
If you see inside the container, I've written one line of code to implement the date & time as,
'${data['date']} | ${data['time'].substring(0,data['time'].indexOf('.'))}'
If you've followed all the steps properly the final view of home_page.dart should look alike,
import 'dart:async';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final StreamController _streamController = StreamController();
Future<void> realTimeClock() async{
var current = DateTime.now().toString().split(' ');
var date = List.from(current[0].split('-').reversed).join('/');
var time = current[1];
var data = {
'date' : date,
'time' : time,
};
_streamController.sink.add(data);
}
@override
void initState() {
// TODO: implement initState
super.initState();
Timer.periodic(const Duration(seconds: 1), (timer) {
realTimeClock();
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_streamController.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: StreamBuilder(
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData){
var data = snapshot.data;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FadeInImage(
fit: BoxFit.fill,
height: MediaQuery.of(context).size.height/2,
image: const NetworkImage('https://media.giphy.com/media/j0qlQDHk2HTVuwImpo/giphy.gif'),
placeholder: AssetImage('assets/images/images.png'),
imageErrorBuilder: (context, error, stackTrace){
return Image.asset(
'assets/images/images.png',
fit: BoxFit.fill,
);
}),
Container(
width: MediaQuery.of(context).size.width /1.2,
child: Text(
'${data['date']} | ${data['time'].substring(0,data['time'].indexOf('.'))}',
style: const TextStyle(
fontSize: 34,
color: Colors.white,
),
),
)
],
),
);
}else if (snapshot.hasError){
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Center(child: Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.white),),),
);
}else{
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: const Center(child: CircularProgressIndicator()),
);
}
},
),
);
}
}
We can notice that I've made the background color black & the color of the text as white to make the clock as shown in the beginning.
That's it, we are now ready to check the implementation by running the application inside the emulator.
Outro
That's it, this was the implementation of the Real-Time Date & Time without even using any background services. Wasn't it fun? I know it's fun coding when you are quite good at it & trust me the path towards that phase begins with the first simple "foo-bar" application... Ask me I am a mechanical engineer who got into this field a few months ago...the thing is, consistency is a myth like all others, & yet we believe in it... as said by a wise(lazy) engineer ๐
so, hello all, I am Rushikesh & this is a short flutter project with one of the powerful concepts used widely. Have fun learning, & will catch you in the next one... till then it's a farewell.