Handling Asynchronous Data in Flutter with Generic Classes

An implementation that defines data states using a generic class and returns widgets based on the current state.

1. Initial Data Declaration

Ds<Profile> profileInfo = Loading(); // or Loading<Profile>();

2. Fetching Data

Future<void> fetchData() async {  
  try {  
    profileInfo = Fetched(  
      User(  
        imgUrl: 'https://avatars.githubusercontent.com/u/75591730?v=4',  
        name: 'Ximya',  
        description:  
            'Lorem ipsum dolor sit amet, consectetur adipiscing ...',  
      ),  
    ); 
    log('Data successfully fetched');  
  } catch (e) {  
    profileInfo = Failed(e);  
    log('Data fetching failed. ${e}');  
  }  
}

3. Returning Widgets Based on States

final profile = controller.profileInfo;

return profile.onState(  
  fetched: (value) => ProfileCard(value),  
  failed: (e) => ErrorIndicator(e),  
  loading: () => CircularProgressIndicator(),  
);

Basic Module

sealed class Ds<T> {
  Ds({required this.state, this.error, this.valueOrNull});

  T? valueOrNull;
  Object? error;
  DataState state;

  T get value => valueOrNull!;

  R onState<R>({
    required R Function(T data) fetched,
    required R Function(Object error) failed,
    required R Function() loading,
  }) {
    if (state.isFailed) {
      return failed(error!);
    } else if (state.isLoading) {
      return loading();
    } else {
      return fetched(valueOrNull as T);
    }
  }
}

class Fetched<T> extends Ds<T> {
  final T data;

  Fetched(this.data) : super(state: DataState.fetched, valueOrNull: data);
}

class Loading<T> extends Ds<T> {
  Loading() : super(state: DataState.loading);
}

class Failed<T> extends Ds<T> {
  final Object error;

  Failed(this.error) : super(state: DataState.failed, error: error);
}

enum DataState {
  fetched,
  loading,
  failed;

  bool get isFetched => this == DataState.fetched;

  bool get isLoading => this == DataState.loading;

  bool get isFailed => this == DataState.failed;
}

GitHub

View Github