flutterdartstateproviderstate-management

Show CircularProgressIndicator in custom button when state app is loading using flutter provider package


I hope someone can help me with this :) I have a flutter app and use the provider package to manage state.

What I want to achieve: When I click on the button and delete an order I want to notify my button and provide the isLoading value. The button should show a loading spinner and lower its opacity when isLoading == true.

What is the current state: When I click on the button the order gets deleted, but neither the opacity get lowered nor the loading spinner gets displayed. Though I have access to isLoading in my button.

This is my OrderService (store):

import ...

class OrderService with ChangeNotifier {
  final supabaseClient = Supabase.instance.client;

  bool _isLoading = false;

  bool get isLoading => _isLoading;

  _toggleLoading() {
    _isLoading = !_isLoading;
    notifyListeners();
  }

  Future<void> deleteOrderWithId(int id, BuildContext context) async {
    try {
      _toggleLoading();
      await supabaseClient.from('orders').delete().eq('id', id);
    } catch (err) {
      print(err);
    } finally {
      _toggleLoading();
    }
  }

  Future<void> createOrderFromOrder(Order order, BuildContext context) async {...}
}

This is my custom button:

import ...

class TrustyButton extends StatelessWidget {
  final void Function() onTap;
  final String text;
  final ButtonType type;
  final Icon? icon;

  TrustyButton({
    super.key,
    required this.onTap,
    required this.text,
    required this.type,
    this.icon,
  });

  final Map<ButtonType, Map<String, Color>> color = {...}

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => OrderService(),
      child: Consumer<OrderService>(
        builder: (context, orderService, _) => GestureDetector(
          onTap: () {
            onTap();
          },
          child: Opacity(
            opacity: orderService.isLoading ? 0.5 : 1,
            child: Container(
              height: 56,
              decoration: BoxDecoration(
                color: color[type]!['backgroundColor']!,
                borderRadius: const BorderRadius.all(
                  Radius.circular(20),
                ),
                boxShadow: [
                  BoxShadow(
                    color: const Color(0x15000000),
                    blurRadius: 10,
                    spreadRadius: 0,
                    offset: Offset.fromDirection(2, 2),
                  ),
                ],
              ),
              child: Center(
                child: orderService.isLoading
                    ? const CircularProgressIndicator(
                        color: Colors.white,
                      )
                    : renderButtonContent(),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Solution

  • You are facing this issue, because you are creating a new instance of the OrderService inside the TrustyButton. Instead you must lift the ChangeNotifierProvider<TrustyButton>, and to use it via a Consumer inside the TrustyButton. Here is a minimal example:

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: ChangeNotifierProvider(
                create: (context) => OrderService(),
                builder: (context, _) {
                  return TrustyButton(
                    onTap: () => context.read<OrderService>().deleteOrderWithId(),
                  );
                },
              ),
            ),
          ),
        );
      }
    }
    
    class OrderService with ChangeNotifier {
      bool _isLoading = false;
    
      bool get isLoading => _isLoading;
    
      void _toggleLoading() {
        _isLoading = !_isLoading;
        notifyListeners();
      }
    
      Future<void> deleteOrderWithId() async {
        try {
          _toggleLoading();
          await Future.delayed(const Duration(seconds: 5));
        } catch (err) {
          print(err);
        } finally {
          _toggleLoading();
        }
      }
    }
    
    class TrustyButton extends StatelessWidget {
      final void Function() onTap;
    
      TrustyButton({
        super.key,
        required this.onTap,
      });
    
      @override
      Widget build(BuildContext context) {
        return Consumer<OrderService>(
            builder: (context, orderService, _) => GestureDetector(
              onTap: onTap,
              child: Opacity(
                opacity: orderService.isLoading ? 0.5 : 1,
                child: Container(
                  height: 56,
                  decoration: BoxDecoration(
                    color: Colors.amberAccent,
                    borderRadius: const BorderRadius.all(
                      Radius.circular(20),
                    ),
                    boxShadow: [
                      BoxShadow(
                        color: const Color(0x15000000),
                        blurRadius: 10,
                        spreadRadius: 0,
                        offset: Offset.fromDirection(2, 2),
                      ),
                    ],
                  ),
                  child: Center(
                    child: orderService.isLoading
                        ? const CircularProgressIndicator(
                            color: Colors.white,
                          )
                        : Text('Delete'),
                  ),
                ),
              ),
            ),
          );
      }
    }