Search code examples
flutterdartgenericsabstractionstrong-typing

Abstraction - How do I create a generic static method for several types to flow through in Dart/Flutter?


Okay, I'm trying to cut back on an enormous amount of boilerplate code for my app. Right now I see the need for some refactoring and serious abstraction.

I want to abstract CRUD actions for my database code. Right now I'm creating a separate file and duplicating the code for each object. I feel like there must be a better way but I don't know how to keep explicit type safety while abstracting with generics.

I'm currently doing something like this for EACH object:
I'm very sure you can see how this would drive someone nuts, I have 45+ objects in my app... and many are more complex than this.

abstract class HouseDatabaseAPI {

  /// CREATE EVENT
  /// Create house for authorized user
  static Future<void> createHouse({required House newHouse}) async {
    Box<House> houseBox = await Hive.openBox<House>('house_box');
    await houseBox.put(newHouse);
  }

  /// READ EVENT
  /// Get house for authorized user
  static Future<House?> getHouse() async {
    Box<House> houseBox = await Hive.openBox<House>('house_box');
    House? house = houseBox.getAt(0);
    return house;
  }

  /// UPDATE EVENT
  /// Update house for authorized user
  static Future<void> updateHouse({House? updatedHouse}) async {
    Box<House> houseBox = await Hive.openBox<House>('house_box');
    await houseBox.putAt(0, updatedHouse!);
  }

  /// DELETE EVENT
  /// Delete house from the local machine
  static Future<void> deleteGroup() async {
    Box<House> houseBox = await Hive.openBox<House>('house_box');
    await houseBox.deleteAt(0);
  }
}

Of course I want this to be strictly typed and NOT dynamic. What I would like to be able to do instead of a massive flow control statement (rough pseudocode):


enum DatabaseAction {
   create,
   read,
   update,
   delete,
}

abstract class DatabaseRoutingAPI {
   
   Future<T> globalDatabaseCreateAction({
      DatabaseAction databaseAction,
      Object object,
      String databaseName,
      }) async {
         Box<T> houseBox = await Hive.openBox<T>(databaseName);
         await houseBox.put(object);
   }      

   ...

}

Solution

  • I will navigate you to one good source from my bookmarks about Hive data handling -> here

    And here I will try to answer your question:

    abstract class Database {
      Box get box;
      T get<T>(String id);
      List<T> getAll<T>();
      Future<void> delete(String id);
      Future<void> deleteAll(List<String> keys);
      Future<void> addUpdate<T>(String id, T item);
    }
    
    class DatabaseImplementing implements Database {
      const DatabaseImplementing(this._box);
      
      final Box _box;
      
      @override
      Box get box => _box;
    
      @override
      T get<T>(String id) {
        try {
          final data = box.get(id);
          
          if (data == null) {
            throw Exception('$T not in the box.');
          }
          
          return data;
        } catch (_) {
          rethrow;
        }
      }
    
      @override
      List<T> getAll<T>() {
        try {
          final data = box.toMap().values;
          
          if (data.isEmpty) {
            throw Exception('$T not in the box.');
          }
    
          return data.toList().cast<T>();
        } catch (_) {
          rethrow;
        }
      }
    
      @override
      Future<void> delete(String id) async {
        try {
          await box.delete(id);
        } catch (_) {
          rethrow;
        }
      }
    
      @override
      Future<void> addUpdate<T>(String id, T item) async {
        try {
          await box.put(id, item);
        } catch (_) {
          rethrow;
        }
      }
    
      @override
      Future<void> deleteAll(List<String> keys) async {
        try {
          await box.deleteAll(keys);
        } catch (_) {
          rethrow;
        }
      }
    }
    

    Sure it's many other ways to do this.