GDG Moi University

Flutter with AI

Google Developer Group
Moi University

Intermediate Flutter Concepts

Taking your Flutter skills to the next level with advanced techniques

State Management

As your Flutter applications grow in complexity, managing state becomes increasingly important. Flutter offers several approaches to state management.

setState

The simplest form of state management in Flutter is using setState() within a StatefulWidget.

class CounterPage extends StatefulWidget { @override _CounterPageState createState() => _CounterPageState(); }  class _CounterPageState extends State<CounterPage> { int _counter = 0;  void _incrementCounter() { setState(() { _counter++; }); }  @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter')), body: Center( child: Text( 'Count: $_counter', style: TextStyle(fontSize: 24), ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, child: Icon(Icons.add), ), ); } }

While setState() works well for simple cases, it becomes cumbersome for larger applications where state needs to be shared across multiple widgets.

InheritedWidget

InheritedWidget is a base class that allows efficient propagation of information down the widget tree. It's the foundation for more advanced state management solutions.

class CounterProvider extends InheritedWidget { final int counter; final Function incrementCounter;  CounterProvider({ Key? key, required this.counter, required this.incrementCounter, required Widget child, }) : super(key: key, child: child);  static CounterProvider of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!; }  @override bool updateShouldNotify(CounterProvider oldWidget) { return counter != oldWidget.counter; } }  // Usage class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); }  class _MyAppState extends State<MyApp> { int _counter = 0;  void _incrementCounter() { setState(() { _counter++; }); }  @override Widget build(BuildContext context) { return CounterProvider( counter: _counter, incrementCounter: _incrementCounter, child: MaterialApp( home: CounterPage(), ), ); } }  class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { final provider = CounterProvider.of(context);  return Scaffold( appBar: AppBar(title: Text('Counter')), body: Center( child: Text( 'Count: ${provider.counter}', style: TextStyle(fontSize: 24), ), ), floatingActionButton: FloatingActionButton( onPressed: () => provider.incrementCounter(), child: Icon(Icons.add), ), ); } }

Provider Package

The Provider package is a wrapper around InheritedWidget that makes it easier to use. It's one of the most popular state management solutions in Flutter.

// First, add the provider package to your pubspec.yaml // dependencies: //   provider: ^6.0.5  import 'package:flutter/material.dart'; import 'package:provider/provider.dart';  // Create a model class class CounterModel extends ChangeNotifier { int _counter = 0;  int get counter => _counter;  void increment() { _counter++; notifyListeners();  // Notify listeners to rebuild } }  // Set up the provider void main() { runApp( ChangeNotifierProvider( create: (context) => CounterModel(), child: MyApp(), ), ); }  // Use the provider in your widgets class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter with Provider')), body: Center( child: Consumer<CounterModel>( builder: (context, model, child) { return Text( 'Count: ${model.counter}', style: TextStyle(fontSize: 24), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Access the model and call increment Provider.of<CounterModel>(context, listen: false).increment(); }, child: Icon(Icons.add), ), ); } }

Provider is recommended by the Flutter team and is a great choice for most applications. There are other state management solutions like Bloc, Redux, and GetX, each with its own strengths and use cases.

Navigation and Routing

Navigation is a crucial part of most applications. Flutter provides a powerful navigation system to move between screens.

Basic Navigation

The simplest way to navigate between screens is using the Navigator class.

// Navigate to a new screen Navigator.push( context, MaterialPageRoute(builder: (context) => SecondScreen()), );  // Go back to the previous screen Navigator.pop(context);  // Replace the current screen Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => ReplacementScreen()), );  // Clear the navigation stack and show a new screen Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => HomeScreen()), (route) => false,  // Remove all previous routes );

Named Routes

For more complex applications, you can use named routes to organize your navigation.

void main() { runApp( MaterialApp( initialRoute: '/', routes: { '/': (context) => HomeScreen(), '/details': (context) => DetailsScreen(), '/settings': (context) => SettingsScreen(), }, ), ); }  // Navigate to a named route Navigator.pushNamed(context, '/details');  // Navigate with arguments Navigator.pushNamed( context, '/details', arguments: {'id': 123, 'title': 'Item Details'}, );  // Retrieve arguments in the destination screen final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;

Navigation 2.0 (Router API)

Flutter 1.22 introduced a new Router API (often called Navigation 2.0) that provides more control over the navigation stack and better support for deep linking and web URLs.

While more complex, it's worth learning for advanced applications, especially those targeting the web or needing deep linking support.

Working with Lists

Lists are a common UI pattern in mobile apps. Flutter provides several widgets for displaying lists efficiently.

ListView

ListView is the most basic list widget in Flutter. It's similar to a Column but with scrolling capabilities.

// Basic ListView with fixed children ListView( children: [ ListTile(title: Text('Item 1')), ListTile(title: Text('Item 2')), ListTile(title: Text('Item 3')), ], );  // ListView.builder for dynamic lists ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text(items[index].title), subtitle: Text(items[index].description), onTap: () { // Handle item tap }, ); }, );  // ListView.separated for lists with separators ListView.separated( itemCount: items.length, itemBuilder: (context, index) { return ListTile(title: Text(items[index])); }, separatorBuilder: (context, index) { return Divider();  // Add a divider between items }, );

GridView

GridView displays items in a 2D grid.

// Basic GridView with fixed children GridView.count( crossAxisCount: 2,  // 2 columns children: [ Card(child: Center(child: Text('Item 1'))), Card(child: Center(child: Text('Item 2'))), Card(child: Center(child: Text('Item 3'))), Card(child: Center(child: Text('Item 4'))), ], );  // GridView.builder for dynamic grids GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: items.length, itemBuilder: (context, index) { return Card( child: Center( child: Text(items[index]), ), ); }, );

Asynchronous Programming

Flutter applications often need to perform operations that take time, such as fetching data from the internet or reading from a database. Dart provides powerful tools for asynchronous programming.

Futures

A Future represents a computation that doesn't complete immediately. It's similar to a Promise in JavaScript.

// Creating a Future Future<String> fetchData() async { // Simulate network request await Future.delayed(Duration(seconds: 2)); return 'Data loaded successfully'; }  // Using a Future void loadData() async { try { String result = await fetchData(); print(result);  // Prints: Data loaded successfully } catch (e) { print('Error: $e'); } }  // Alternative syntax using then/catchError fetchData() .then((result) => print(result)) .catchError((error) => print('Error: $error'));

FutureBuilder

FutureBuilder is a widget that makes it easy to work with asynchronous data in your UI.

FutureBuilder<String>( future: fetchData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator();  // Loading indicator } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}');  // Error state } else { return Text(snapshot.data!);  // Success state } }, )

Streams

A Stream is a sequence of asynchronous events. It's useful for data that changes over time, like user input or real-time updates.

// Creating a Stream Stream<int> countStream(int max) async* { for (int i = 1; i <= max; i++) { await Future.delayed(Duration(seconds: 1)); yield i;  // Emit a value to the stream } }  // Using a Stream void listenToStream() { final stream = countStream(5);  stream.listen( (data) => print('Data: $data'),  // Called for each emitted value onError: (error) => print('Error: $error'), onDone: () => print('Stream completed'), ); }  // StreamBuilder for UI StreamBuilder<int>( stream: countStream(5), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { return Text('Count: ${snapshot.data}'); } else { return Text('No data'); } }, )

Handling User Input and Forms

Forms are a common way to collect user input. Flutter provides a Form widget and various form fields to make form handling easier.

Form and FormField

// Create a Form with a GlobalKey final _formKey = GlobalKey<FormState>();  Form( key: _formKey, child: Column( children: [ TextFormField( decoration: InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!value.contains('@')) { return 'Please enter a valid email'; } return null;  // Return null for valid input }, ), TextFormField( decoration: InputDecoration(labelText: 'Password'), obscureText: true,  // Hide password validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } if (value.length < 6) { return 'Password must be at least 6 characters'; } return null; }, ), ElevatedButton( onPressed: () { // Validate the form if (_formKey.currentState!.validate()) { // Form is valid, process the data _formKey.currentState!.save(); // Submit the form } }, child: Text('Submit'), ), ], ), )

Controlling Form Fields

You can use TextEditingController to control the content of form fields.

class MyFormState extends State<MyForm> { // Create controllers final _emailController = TextEditingController(); final _passwordController = TextEditingController();  @override void dispose() { // Clean up controllers when the widget is disposed _emailController.dispose(); _passwordController.dispose(); super.dispose(); }  @override Widget build(BuildContext context) { return Form( child: Column( children: [ TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), ), TextFormField( controller: _passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true, ), ElevatedButton( onPressed: () { // Access the values final email = _emailController.text; final password = _passwordController.text; print('Email: $email, Password: $password'); }, child: Text('Submit'), ), ], ), ); } }

Pro Tip

When working with forms, consider using packages like form_field_validator for more advanced validation or flutter_form_builder for a more comprehensive form solution. These packages can save you time and provide more features than the built-in form widgets.