flutter weather app

Flutter Weather app with Clean Architecture and Bloc State Management.

Khalid Meftuh
10 min readJan 19, 2024

--

This app is based on flutter app design competition which is held on HeyFlutter.com, Although i have started and become busy in between i have done it so that it can help out developers for reference i assume you know little bit about bloc state management, clean architecture and best practices.(Note:-Code you get from git can be more and more optimized)

So after watching the design i have put three logics,

  1. Getting weather API responses.
  2. Getting city images and saving those.
  3. Getting weekly forecasts.

So on this app development for weather API i have used OpenWeather API which is enough but limited but works fine for testing, the second issue was challenging but after digging some information i found out unsplash provides city images which will help us to get city images and save those URL in local database. For weekly forecasts i just created mock data for the population. So before we start i can consider Bloc state management, what is dependency injection and some hints for clean architecture. So project link will be attached at bottom and if you like the project don’t for get to give it clap and flow me.

Here we will discuss just the main components of the project. The very first issue is getting user location and sending user city to open weather API and getting those responses. First create an account on openweather and So to you need to have an API key, so to get user location i have used GeoLocator and Geocoding flutter libraries. Other libraries are connectivity check, dependency injection, dartz and other listed below.

dependencies:
flutter:
sdk: flutter


# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
# responsive screen and text size
flutter_screenutil:
# network request
dio:
# network request response
either_dart:
# render svg images
flutter_svg_provider:
# dependency injection
get_it:
# internate conncetivity
connectivity_plus:
# state management
flutter_bloc:
equatable:
geolocator:
geocoding:
lottie:
weather_icons:
glassmorphism_ui:
path_provider:
sembast:
intl:
shared_preferences:
permission_handler:
home_widget:
localization:
shimmer:
fluttertoast:

So on this design we have two screens one home page and other is saved cities list, so in my on home page i need to check whether am coming from saved list using this code.

Add you API keys in weather services class

class WeatherAppServices{
static const String apiKey="";
static const String baseURL="https://api.openweathermap.org/data/2.5/weather";
static const String unSplashBaseURL="https://api.unsplash.com/search/photos";
static const String unSplashApiKey ="";
/// icon data URL
static const String iconURL="https://openweathermap.org/img/wn/";
static const String iconSize="@2x.png";
}
class WeatherAppHomePage extends StatefulWidget {
/// user is comming back to see saved pages so if you are comming from saved pages this can be true
bool? showDataFromSavedCities;
/// user is comming back to see saved pages so if you are comming from saved pages this can be true
WeatherModel? cityModel;

WeatherAppHomePage({super.key, this.showDataFromSavedCities, this.cityModel});

Hmm now user will be asked to enable location permission and we need to leave the app and get back to the app so after permission is given we need to ask how location be accessed and get user city. So we will go for Widgets Binding i have write article here. So here is the implementation

class _WeatherAppHomePageState extends State<WeatherAppHomePage>
with WidgetsBindingObserver,

so our class will observe the state of the app like paused, resumed so we will use this powerful future with user activity.


@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// if user is comming from saved cities i dont want to init location service since it already have some data
if (widget.showDataFromSavedCities == false) {
initLocationService();
} else {
/// for testing purpose
if (kDebugMode) {
print("City Images");
print(widget.cityModel?.cityImageURL);
}
}
}

@override
void dispose() {
// dispose observer
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

so when users gets to home page if location service is disabled it will be promoted to enable location service.

So when user leaves the app the app state will be paused and user will enable location service and comes back to app and the state will be resumed, so when user allowed and comes back we will invoke on resume binding observer.

  @override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (!isLocationServiceInitialized) {
LocationPermission permission = await Geolocator.checkPermission();
if (permission != LocationPermission.denied) {
await initLocationService();
isLocationServiceInitialized = true;
} else {
/// this specially helps when user starts the for the first time will help to show user needs to use this services like while useing the app or anything
await initLocationService();
}
}
}
}

/// so initLocation service will help us to show those dialogs or handle location permission
Future<void> initLocationService() async {
if (!await Geolocator.isLocationServiceEnabled()) {
await showLocationServiceDialog();
} else {
dismissPermissionDialog();
await handleLocationPermission();
}
}

Future<void> showLocationServiceDialog() async {
/// mounted means does this exits in widget tree?
/// if not we cannot show those alrets on async gaps we need to check first
if (!mounted) return;

showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(WeatherAppString.locationDisabled),
content: Text(WeatherAppString.pleaseEnableLocation),
actions: <Widget>[
TextButton(
onPressed: () async {
await Geolocator.openLocationSettings();
if (mounted) {
Navigator.of(context).pop();
}
},
child: Text(WeatherAppString.openSettings),
),
],
);
},
);
}

Future<void> handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
switch (permission) {
case LocationPermission.denied:
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (mounted) permissionDialog(context);
return;
} else if (permission == LocationPermission.deniedForever) {
/// automatically routing user to settings page to enable settings
if (mounted) {
await openAppSettings();
}
return;
}
break;
case LocationPermission.deniedForever:
if (mounted) {
await openAppSettings();
}

return;
default:
break;
}
await getPosition();
}

Future<void> getPosition() async {
try {
await getUserPosition();
} catch (e) {
/// incase some exceptions happend
if (mounted) permissionDialog(context);
}
}


Future<void> getUserPosition() async {
if (isGettingUserPosition) {
return;
}

isGettingUserPosition = true;
try {
if (!await Geolocator.isLocationServiceEnabled()) return;

LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) return;

/// get ser position
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.best);
List<Placemark> locationPlaceMark =
await placemarkFromCoordinates(position.latitude, position.longitude);
Placemark place = locationPlaceMark[0];

if (!mounted) {
} else {
/// the if statement here is just to enforce since the if loop in intit statement will
/// protect us before we reach here
if (widget.showDataFromSavedCities == false) {
/// we passed city name to our event so that we can get city weather condition
/// on from call place.locality is cityname.
final weatherCityBloc = BlocProvider.of<HomeControllerBloc>(context);
weatherCityBloc.add(GetCurrentCityWeatherInfo(place.locality!));
}
}
} finally {
isGettingUserPosition = false;
}
}

void permissionDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
key: _permissionDialogKey,
title: Text(WeatherAppString.locationServicesDisabled),
content: Text(WeatherAppString.locationEnable),
actions: <Widget>[
MaterialButton(
onPressed: () {
/// the pop will remove the dialog.
Navigator.of(context).pop();
getPosition();
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Text(WeatherAppString.okay),
),
GestureDetector(
onTap: () {
/// user didnt want to go to settings and enable permission we exit
exit(0);
},
child: Text(WeatherAppString.cancel),
),
],
),
)
],
);
},
);
}

The other challenge is removing permission dialog after user go to give location permission that is the key point. So here i used concept of key and removed it successfully. if we didn’t remove the dialog will stay pop up.

 void dismissPermissionDialog() {
if (_permissionDialogKey.currentState != null &&
_permissionDialogKey.currentContext != null) {
Navigator.of(_permissionDialogKey.currentContext!).pop();
}
}

So the other option is getting next four days example today is Thursday so we need to get Friday, Saturday , Sunday and Monday. I used the following method to get the next four days.

  /// get next four days
/// this fun returns list of string of next four days. Just for mock up
/// if api works for free i would give us those dates and it will work

static List<String> getNextFourDays() {
List<String> daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

DateTime currentDate = DateTime.now();
int todayIndex = currentDate.weekday % 7;

List<String> nextDays = [];
for (int i = 1; i <= 4; i++) {
DateTime nextDate = currentDate.add(Duration(days: i));
String day = daysOfWeek[nextDate.weekday % 7];
String dayWithNumber = '$day ${nextDate.day}';
nextDays.add(dayWithNumber);
}

return nextDays;
}


/// then the following bloc will help us get mock response from assets file
final forecastBloc = BlocProvider.of<GetDailyForecastBloc>(context);
forecastBloc.add(GetDailyForCast());

This is my mock JSON

{
"daily": [
{
"summary": "Expect a day of partly cloudy",
"temp": {
"day": 17,
"min": 8,
"max": 17.5
},
"pressure": 1016,
"humidity": 59,
"dew_point": 290.48,
"wind_speed": 3.98,
"wind_deg": 76,
"wind_gust": 8.92,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
]
},
{
"summary": "Expect a day of Sunny",
"temp": {
"day": 23,
"min": 20,
"max": 22
},
"pressure": 1016,
"humidity": 59,
"dew_point": 290.48,
"wind_speed": 2,
"wind_deg": 76,
"wind_gust": 8.92,
"weather": [
{
"id": 500,
"main": "Sunny",
"description": "light rain",
"icon": "01d"
}
]
},
{
"summary": "Expect a day of few cloud",
"temp": {
"day": 19,
"min": 15,
"max": 19
},
"pressure": 1016,
"humidity": 59,
"dew_point": 290.48,
"wind_speed": 2.98,
"wind_deg": 76,
"wind_gust": 8.92,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "02d"
}
]
},
{
"summary": "Expect a day of rain",
"temp": {
"day": 10,
"min": 5,
"max": 11
},
"pressure": 1016,
"humidity": 59,
"dew_point": 290.48,
"wind_speed": 3.98,
"wind_deg": 76,
"wind_gust": 8.92,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
]
}
],
"comments": [
"The forecast for the upcoming day is paid which is unsupported for Ethiopia.",
"Therefore, only a 4-day forecast is provided."
]
}

So the above sample codes we have seen will help you understand main functionalities on our home widget.

How did i display weather icons? The api will support to display icons but uses another URL to show us, so for simplification on utils class i have used the following, to return for me weather icons.

  IconData getWeatherIcon(String weatherCode) {
switch (weatherCode) {
case "01d":
return WeatherIcons.day_sunny;
case "02d":
return WeatherIcons.day_cloudy;
case "03d":
return WeatherIcons.cloud;
case "04d":
return WeatherIcons.cloudy;
case "09d":
return WeatherIcons.showers;
case "10d":
return WeatherIcons.rain;
case "11d":
return WeatherIcons.thunderstorm;
case "13d":
return WeatherIcons.snow;
default:
return WeatherIcons.refresh; // A fallback icon in case of unknown code
}
}

/// sample usage on home widget
Icon(AppUtils().getWeatherIcon( widget.cityModel!.weather[0].icon),
size: 100.0,
color: WeatherAppColor.yellowColor,
)
/// icon dont exits on our utils function get from network
: Image.network(
AppUtils().getWeatherIconURL(
widget.cityModel!.weather[0].icon,
),
color: WeatherAppColor.yellowColor,
),
String getWeatherIconURL(String weatherCode) {

return WeatherAppServices.iconURL+weatherCode+WeatherAppServices.iconSize;



}

So now lets go to save and sync weather condition on saved cities page

class CitiesList extends StatelessWidget {
/// some times cities cannot be found example very local cities so
/// when comming from home page we need to specify why we are here do miss user
/// city? or are we here to check other cities?
final bool isCurrentCityNotFound;

const CitiesList({super.key, required this.isCurrentCityNotFound});

@override
Widget build(BuildContext context) {
/// this bloc will be responsible to load saved cities weather from local db(HIVE)
final userCityBloc = BlocProvider.of<UserCityControllerBloc>(context);
userCityBloc.add(const FetchSavedCitiesData());
return UserCities(isCurrentCityNotFound: isCurrentCityNotFound);
}
}


TextEditingController saveNewCityTextController = TextEditingController();
/// will help us to get cityWeatherModel so that we could iterate and populate
/// data
List<WeatherModel> cityNamesData = [];
bool isSyncing = false;

@override
void dispose() {
/// don't for get to dispose any controller
saveNewCityTextController.dispose();
super.dispose();
}

/// sync bloc
IconButton(
icon: Icon(
Icons.sync,
color: WeatherAppColor.whiteColor,
size: 30,
),
onPressed: () {
/// check if list is not empty( if empty no sync
/// other wise use connectivity bloc to prevent sync
/// a task for you
if (cityNamesData.isNotEmpty) {
final syncBloc = BlocProvider.of<SyncDatabaseBloc>(context);
syncBloc.add(SyncMyData(cityNamesData));
}
},
),
               if (state is UserCityLoaded) {
/// city is loaded and we need to check if user have current city
/// i mean as i mentioned above if city didnt found we can take the first element as user city
/// and upadte home widget
if (isSyncing) {
final currentCityWeatherModel = state.usermodel.firstWhere(
(element) => element.isCurrentCity == true,
orElse: () => state.usermodel.first,
);

AppUtils.updateHomeScreenWidget(currentCityWeatherModel);
}

Did you read home widget? yeah you read it how to implement it read my article here. But this is bit advanced it supports weather icon loading from live internate the first ever implementation in flutter.

So user needs to add new cities weather to discover as soon as user hits save button our bloc emits either success or failure like below

listener: (context, listenerState) {

if (listenerState is CityWeatherLoaded) {
/// user city weather is loaded from api so save it local db
saveNewCityTextController.clear();
WeatherModel newModel = listenerState.usermodel;
newModel.cityImageURL = listenerState.usermodel.cityImageURL;
newModel.isCurrentCity = false;
AppUtils.saveUserCity(newModel, context);
}
if (listenerState is UserCityFetchingError) {
/// user city weather is not found so fetche saved data from database and show toast
saveNewCityTextController.clear();
AppUtils.showToastMessage(
WeatherAppString.noWeatherInfo, Toast.LENGTH_SHORT);
final userCityBloc =
BlocProvider.of<UserCityControllerBloc>(context);
userCityBloc.add(const FetchSavedCitiesData());
}
},
),
demos

So if you have any questions or doubts put it here we can discuss more on this otherwise if you like it please clap and follow me on LinkedIn you will be benefited, next i will put the some code with provider and getx state management so keep following on GitHub or LinkedIn to get updates Project code you like it Clap and Follow.

--

--

Khalid Meftuh

Driven by the desire to share knowledge and empower fellow developers, on Flutter and Android.