Flutter Course: Finishing the Weather App: 15.11

In this course, we will go through the final stages of completing a weather app using Flutter. In this section, we will cover several important elements such as optimizing the app’s user interface (UI), data handling, and API integration. All of these processes are aimed at providing a better experience for the user.

Project Environment Setup

First, ensure that the project is properly set up. Make sure you have installed the Flutter SDK along with Android Studio or Visual Studio Code. Also, to ensure that the Flutter environment is correctly configured, run the following command in the terminal:

flutter doctor

This command will check all configured environments and notify you if there are any additional setups required.

Integrating the API

The core of the weather application is the data source. We will use the OpenWeatherMap API to fetch real-time weather data. To get an API key, sign up on the OpenWeatherMap website and obtain your key.

We will use the HTTP library to call the API. Use the following command to add the HTTP library:

flutter pub add http

Next, let’s implement a function to send HTTP requests:

import 'package:http/http.dart' as http;

Future fetchWeatherData(String city) async {
    final apiKey = 'YOUR_API_KEY';
    final url = 'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=$apiKey&units=metric';
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
        // Data processing logic
    } else {
        throw Exception('Failed to load weather data');
    }
}

Data Modeling

To model the weather data, we need to create a model class that can map the JSON data to the class. For example:

class Weather {
    final String cityName;
    final double temperature;
    final String description;

    Weather({required this.cityName, required this.temperature, required this.description});

    factory Weather.fromJson(Map json) {
        return Weather(
            cityName: json['name'],
            temperature: json['main']['temp'],
            description: json['weather'][0]['description'],
        );
    }
}

State Management

In Flutter, there are various ways to manage state. In this example, we will use the Provider pattern. We will add the Provider package and implement the WeatherProvider class:

import 'package:flutter/material.dart';

class WeatherProvider with ChangeNotifier {
    Weather? _weather;

    Weather? get weather => _weather;

    Future getWeather(String city) async {
        // API call and data fetching
        final data = await fetchWeatherData(city);
        _weather = Weather.fromJson(data);
        notifyListeners();
    }
}

Building the UI

Now let’s implement the most important and interesting part, the UI. Using Flutter widgets, we will create a simple yet intuitive user interface. The basic structure is as follows:

Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text('Weather App'),
        ),
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                    Text('City Name: ${weatherProvider.weather?.cityName}'),
                    Text('Temperature: ${weatherProvider.weather?.temperature}°C'),
                    Text('Condition: ${weatherProvider.weather?.description}'),
                ],
            ),
        ),
    );
}

Updating State

The UI needs to be updated whenever the state changes. To do this, use the Consumer widget from Provider to detect state changes and rebuild the UI:

Consumer(
    builder: (context, weatherProvider, child) {
        if (weatherProvider.weather != null) {
            return WeatherDisplay(weather: weatherProvider.weather!);
        } else {
            return CircularProgressIndicator(); // Loading data
        }
    },
)

Final Steps and Build

Once all the code is written, it’s time to run the app and check the results. Enter the following command in the terminal to run the app:

flutter run

With that, a simple weather application using Flutter has been completed. Additionally, you can add various features to improve the user experience. For example, adding search functionality and location-based services can make it more convenient for users to check weather information.

Conclusion

Through this course, you learned the fundamental process of building a weather app using Flutter, including user interfaces, API usage, and state management. Try to continuously develop the app by adding and improving various features. Thank you.

Flutter Course: Adding a Loading Indicator in 15.10

Flutter is an open-source UI software development kit (SDK) developed by Google, supporting the creation of high-performance applications across various platforms, including mobile, web, and desktop apps. Flutter comes with many features that allow for rapid building and deployment of entire applications. In this tutorial, we will explain how to add a loading indicator to a Flutter application. Loading indicators are important elements that visually signal to users that a task is in progress, significantly enhancing the user experience.

1. What is a Loading Indicator?

A loading indicator is a UI element that signals to users that data is being loaded. It helps users understand the responsiveness of the app and alleviates concerns about potential delays. Flutter offers various loading indicators such as:

  • LinearProgressIndicator: Horizontal progress bar
  • CircularProgressIndicator: Circular progress bar
  • Custom loading indicators: Can be customized utilizing Flutter’s flexibility

2. Project Setup

To add a loading indicator, an existing Flutter project is required. To create a new Flutter project, run the following command to generate the basic project structure.

flutter create loading_indicator_example

Navigate to the created project directory and open the project using an IDE (e.g., Visual Studio Code).

cd loading_indicator_example

3. Adding a Loading Indicator

To add a loading indicator, we first need to define the user interface (UI). The following code is an example that uses CircularProgressIndicator and LinearProgressIndicator.


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Loading Indicator Example',
      home: LoadingIndicatorDemo(),
    );
  }
}

class LoadingIndicatorDemo extends StatefulWidget {
  @override
  _LoadingIndicatorDemoState createState() => _LoadingIndicatorDemoState();
}

class _LoadingIndicatorDemoState extends State {
  bool _isLoading = false;

  void _fetchData() async {
    setState(() {
      _isLoading = true;
    });

    // Simulate a network request
    await Future.delayed(Duration(seconds: 3));

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Loading Indicator Example'),
      ),
      body: Center(
        child: _isLoading 
            ? CircularProgressIndicator() 
            : ElevatedButton(
                onPressed: _fetchData, 
                child: Text('Fetch Data'),
              ),
      ),
    );
  }
}

The above code uses CircularProgressIndicator to show the loading state to the user. When the button is clicked, the _fetchData function is called, which waits for 3 seconds before hiding the loading indicator.

4. Adding a LinearProgressIndicator

You can implement the same way to show the loading state using LinearProgressIndicator. To change the layout, simply modify the code.


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Loading Indicator Example',
      home: LoadingIndicatorDemo(),
    );
  }
}

class LoadingIndicatorDemo extends StatefulWidget {
  @override
  _LoadingIndicatorDemoState createState() => _LoadingIndicatorDemoState();
}

class _LoadingIndicatorDemoState extends State {
  bool _isLoading = false;

  void _fetchData() async {
    setState(() {
      _isLoading = true;
    });

    await Future.delayed(Duration(seconds: 3));

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Loading Indicator Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _isLoading 
                ? LinearProgressIndicator() 
                : ElevatedButton(
                    onPressed: _fetchData, 
                    child: Text('Fetch Data'),
                  ),
        ],
      )
    );
  }
}

5. Customizing Loading Indicators

You can customize the default loading indicators provided by Flutter to create a more unique and appealing UI. The following is an example of customization by adjusting color, size, and shape.


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Customized Loading Indicator Example',
      home: LoadingIndicatorDemo(),
    );
  }
}

class LoadingIndicatorDemo extends StatefulWidget {
  @override
  _LoadingIndicatorDemoState createState() => _LoadingIndicatorDemoState();
}

class _LoadingIndicatorDemoState extends State {
  bool _isLoading = false;

  void _fetchData() async {
    setState(() {
      _isLoading = true;
    });

    await Future.delayed(Duration(seconds: 3));

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Customized Loading Indicator Example'),
      ),
      body: Center(
        child: _isLoading 
          ? CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation(Colors.green),
              strokeWidth: 10.0,
            ) 
          : ElevatedButton(
              onPressed: _fetchData, 
              child: Text('Fetch Data'),
            ),
      ),
    );
  }
}

6. Conclusion

Loading indicators are important elements that improve the user experience and maintain the identity of the application. With Flutter, you can easily add loading indicators and customize them as needed. We hope this tutorial helps you successfully add loading indicators to your Flutter applications.

7. Additional Resources

For more information about loading indicators, please refer to the official Flutter documentation or community forums. They contain various examples and tips that will be useful for troubleshooting.

Thank you!

Flutter Course: 14.7 Stream and StreamBuilder

Flutter is a powerful framework that supports asynchronous programming. Among its features, Stream and StreamBuilder are essential for handling the flow of data. In this article, we will take a deep dive into the concepts of Stream and StreamBuilder, their usage, and real examples.

1. What is Stream?

Stream is a concept in reactive programming that allows asynchronous data transmission. It can receive and process data whenever it is generated. For example, Stream can be used to continuously fetch data from a server or to handle user input in real-time.

1.1 Basic Concept of Stream

Stream generates a series of asynchronous events, and there are ‘listeners’ that consume these events. There are various types of Streams, with the two most common being Future, which produces a single value, and Stream, which produces multiple values.

1.2 Key Features of Stream

  • Data flow: Stream transmits data every time it is generated.
  • Asynchronous processing: Stream processes data asynchronously to keep the UI smoother.
  • Listeners: Stream requires listeners to consume the data.
  • Diverse sources: Stream can receive data from multiple sources, such as web sockets, HTTP requests, or user inputs.

2. What is StreamBuilder?

StreamBuilder is a widget in Flutter that is used to reflect Stream data in the UI. StreamBuilder is connected to a Stream and automatically updates the UI whenever new data is generated.

2.1 Structure of StreamBuilder

StreamBuilder is typically used in the following form:

StreamBuilder(
  stream: yourStream,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    // Process the data and build the UI.
  },
);

Here, yourStream is the location where data is generated, and a listener for this Stream is automatically created. The builder parameter is a function that receives the Snapshot and generates the UI.

2.2 AsyncSnapshot

The AsyncSnapshot object received in the builder function of StreamBuilder contains the status and data of the Stream. This allows for easy management of the data loading state, error occurrence, and more.

3. Examples of Using Stream and StreamBuilder

Now, let’s explore how to use Stream and StreamBuilder through actual code. In this example, we will create a simple application that periodically sends the current time as a stream and displays it using StreamBuilder.

3.1 Creating a Stream

First, let’s create a Stream that periodically sends the current time.

Stream<DateTime> getTimeStream() async* {
  while (true) {
    yield DateTime.now();
    await Future.delayed(Duration(seconds: 1));
  }
}

The above code returns a Stream that generates DateTime objects every second. It defines an asynchronous function using the async* keyword and triggers the value using yield.

3.2 Building UI with StreamBuilder

Now, let’s use StreamBuilder to display the Stream we created above in the UI.

class TimeDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<DateTime>(
      stream: getTimeStream(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Text('Loading...');
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        } else {
          return Text('Current time: ${snapshot.data}');
        }
      },
    );
  }
}

The above code is an example of using StreamBuilder to display the time. When loading, it shows ‘Loading…’, when an error occurs, it displays the error message, and when the data is successfully received, it shows the current time.

4. Use Cases of Stream and StreamBuilder

Stream and StreamBuilder can be utilized in various contexts. Here are a few examples where they can be applied:

4.1 Real-time Data Communication

In a chat application, for instance, it must receive messages sent by users in real-time. In this case, Stream can be used to receive new messages, and StreamBuilder can be used to update the UI.

4.2 Periodic Data Updates

When fetching periodically updated data, such as stock prices or weather information, Stream can be used. This allows users to always receive the latest information.

4.3 User Event Handling

Apps that provide real-time feedback by detecting user inputs or actions can also utilize Stream. For example, while a user is filling out a form, it can perform real-time validation for each field and provide feedback.

5. Pros and Cons of Stream and StreamBuilder

5.1 Advantages

  • Asynchronous Processing: Increases the responsiveness of the UI by processing data asynchronously.
  • Real-time Updates: Automatically updates the UI every time the data changes.
  • Complex Data Flow Management: Easily manages multiple data sources and complex data flows.

5.2 Disadvantages

  • Complexity: Asynchronous programming can feel complex for beginners.
  • Resource Consumption: Maintaining unnecessary Streams can waste system resources.
  • Error Handling: Consistent handling of errors that can occur in asynchronous operations is necessary.

6. Conclusion

In this article, we explored Stream and StreamBuilder in Flutter in detail. Stream is a powerful tool for transmitting data asynchronously, and StreamBuilder is a widget that helps easily apply this to the UI. I hope this article has helped you understand these two concepts through real examples. Asynchronous programming can be somewhat challenging at first, but you will become more familiar with it through various use cases. Actively use these two concepts in developing fun and useful applications with Flutter!

7. References

Flutter Course: 14.6 Outputting Future in the App

In this course, we will explain in detail how to create and output Future in Flutter. Future is a core concept in Dart’s asynchronous programming, representing an object that returns a value or error when an asynchronous task is completed. This allows us to perform time-consuming tasks without blocking the UI.

1. What is Future?

Future is an object that represents a future result in asynchronous programming, allowing you to wait for the asynchronous task to complete and receive the result. For example, when fetching data through an HTTP request, you can proceed with other UI tasks without waiting for the request to finish.

1.1 States of Future

  • Pending: State where the task has not yet completed.
  • Completed: State where the task has completed and returned a result.
  • Error: State where an error occurred during the task.

2. Creating a Future

There are two main ways to create a Future. The first is to use built-in methods, and the second is to create it through custom functions.

2.1 Using Built-in Methods

You can create a Future object that completes after a certain time using the Future.delayed method. The following is an example that returns a message after 2 seconds.

Future fetchData() {
        return Future.delayed(Duration(seconds: 2), () {
            return 'Data loading complete';
        });
    }

2.2 Creating Custom Functions

You can directly create a function that returns a Future to handle database or API requests. For example, let’s create a function to fetch user information from an API.

Future fetchUser(int userId) async {
        final response = await http.get('https://api.example.com/user/$userId');
        if (response.statusCode == 200) {
            return User.fromJson(json.decode(response.body));
        } else {
            throw Exception('Failed to load user');
        }
    }

3. Outputting Futures

Now, let’s look at how to output the values of the Future object we created in a Flutter app. To do this, we use the FutureBuilder widget. The FutureBuilder dynamically updates the UI based on the state of the Future.

3.1 Using FutureBuilder

To use FutureBuilder, you need to define the future and builder parameters. You specify the Future object that performs the asynchronous task in future, and define the function that builds the UI based on the state of the asynchronous task in builder.

class UserProfile extends StatelessWidget {
        final int userId;
        UserProfile(this.userId);
        
        @override
        Widget build(BuildContext context) {
            return FutureBuilder(
                future: fetchUser(userId),
                builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                        return CircularProgressIndicator();
                    } else if (snapshot.hasError) {
                        return Text('Error: ${snapshot.error}');
                    } else {
                        return Text('User Name: ${snapshot.data.name}');
                    }
                },
            );
        }
    }

3.2 Exception Handling

Since errors may occur during asynchronous tasks, it’s important to handle errors in FutureBuilder using snapshot.hasError. Proper exception handling can provide clear feedback to users.

4. Complete Code Example

Based on what we have learned so far, let’s look at a complete example. This example is a Flutter app that calls an API to fetch user information and displays it on the screen.

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

class User {
    final String name;
    
    User({required this.name});
    
    factory User.fromJson(Map json) {
        return User(name: json['name']);
    }
}

Future fetchUser(int userId) async {
    final response = await http.get('https://api.example.com/user/$userId');
    if (response.statusCode == 200) {
        return User.fromJson(json.decode(response.body));
    } else {
        throw Exception('Failed to load user');
    }
}

class UserProfile extends StatelessWidget {
    final int userId;
    UserProfile(this.userId);
    
    @override
    Widget build(BuildContext context) {
        return FutureBuilder(
            future: fetchUser(userId),
            builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                    return CircularProgressIndicator();
                } else if (snapshot.hasError) {
                    return Text('Error: ${snapshot.error}');
                } else {
                    return Text('User Name: ${snapshot.data.name}');
                }
            },
        );
    }
}

void main() => runApp(MaterialApp(home: Scaffold(body: UserProfile(1))));

5. Conclusion

In this course, we learned how to create Future in Flutter and efficiently handle asynchronous operations using it. The UI composed with FutureBuilder can change dynamically based on data loading states, which is very useful. These asynchronous programming techniques greatly help improve user experience even in complex apps.

Tip: If you want to delve deeper into asynchronous programming, also study the concepts of Stream and async/await.

Flutter Course: 14.5 try/catch Block

Error handling is an important part of computer programming. It enhances the stability and reliability of programs, helping users to use the program smoothly even in unexpected situations. An important feature for error handling in Flutter is the try/catch block. In this tutorial, we will explore how to use the try/catch block in Flutter.

1. Importance of Error Handling

Error handling is a way to manage various exceptional situations that may occur while an application is running. For instance, it uses error handling to prevent the program from crashing abnormally in situations such as network request failures or incorrect user input. Flutter provides this error handling mechanism so that developers can create more reliable applications.

2. Basic Structure of try/catch Block

The try/catch block generally has the following structure:

    
    try {
        // Code that is likely to cause an error
    } catch (e) {
        // Code that runs when an error occurs
    }
    
    

In the above structure, the code within the try block executes normally. However, if an error occurs in this code, the error will be caught in the catch block. This allows the program to avoid abnormal termination and display an appropriate error message to the user.

3. Example: Basic use of try/catch

Below is a simple example of using a try/catch block in Flutter. This example assumes a situation where the user attempts to divide a number by 0. Normally this would cause an error, which can be handled using try/catch.

    
    void divideNumbers(int a, int b) {
        try {
            var result = a ~/ b; // Integer division
            print("Result: $result");
        } catch (e) {
            print("Error occurred: $e");
        }
    }
    
    

If the user inputs 0 in the above code, an error will occur during the execution of the ~/ operator, which will be handled in the catch block.

4. Handling Specific Errors

There are various types of errors that can occur in the catch block. Flutter provides ways to specify these errors. For example, specific errors like FormatException or IntegerDivisionByZeroException can be handled.

    
    void divideNumbers(int a, int b) {
        try {
            var result = a ~/ b;
            print("Result: $result");
        } catch (e) {
            if (e is IntegerDivisionByZeroException) {
                print("Error: Cannot divide by 0.");
            } else {
                print("Error occurred: $e");
            }
        }
    }
    
    

The code above provides clearer information to the user regarding the error that occurs when attempting to divide by 0.

5. Using try/catch in Asynchronous Code

In Flutter, try/catch blocks can also be used in asynchronous code. When errors occur in asynchronous code, the method for error handling when using the await keyword is as follows:

    
    Future fetchData() async {
        try {
            var response = await http.get('https://api.example.com/data');
            // Data processing code
        } catch (e) {
            print("Asynchronous error occurred: $e");
        }
    }
    
    

The code above demonstrates a situation where an error might occur while fetching data through an HTTP request.

6. Throwing Exceptions (throw)

Developers can throw exceptions directly when certain conditions are not met. The throw keyword can be used for this. For instance, if the user’s input is invalid, a custom exception can be created and thrown:

    
    void validateInput(String input) {
        if (input.isEmpty) {
            throw FormatException("Input is empty.");
        }
    }
    
    

The code above shows an example of throwing an exception directly when user input is found to be empty through validation.

7. Custom Exception Classes

In Flutter, developers can create custom exception classes for more detailed error handling. Below is an example of a custom exception class:

    
    class CustomException implements Exception {
        String cause;
        CustomException(this.cause);
    }

    void performOperation() {
        throw CustomException("Custom exception occurred");
    }
    
    

As shown in the example above, you can define the CustomException class and utilize it. This exception can be appropriately handled in the catch block.

8. Conclusion

The try/catch block is a very useful tool for error handling in Flutter. It helps maximize the program’s stability and user experience. I hope you have learned about various error handling mechanisms that can be applied to different situations, from basic usage to asynchronous handling and custom exceptions. May this be helpful for your future Flutter development.

9. References