flutterdartdesign-patternscallbackdecorator

Python decorator-like design pattern for Dart/Flutter?


I'd like to have common try/catch/finally logic in a decorator-like feature that can "wrap" a function or class method. Consider the scenario:

Class MyClass {
  void someMethodA() {
    doSomeInitialWork();
    
    try {
      doSomething();
    } catch (err) {
      throw err;
    } finally {
      doSomeCleanUpWork();
    }
  }

  void someMethodB() {
    doSomeInitialWork();
    
    try {
      doSomethingElse();
    } catch (err) {
      throw err;
    } finally {
      doSomeCleanUpWork();
    }
  }
}

So on and so forth. The unique parts of each method are just the try body. If I have a bunch of methods, some which require the same logic, is there a "nice" way to avoid redundant code?

Ideally it could be syntax like:

@wrapper
void someMethodA() {
  doSomething();
}

@wrapper
void someMethodB() {
  doSomethingElse();
}

MyClassInstance.someMethodA(); // call it like this and the wrapper takes care of everything

but I know those are annotations in Dart and not applicable here.

UPDATE

Following jamesdlin answer, I am trying to incorporate the anonymous function solution to a futures/async/await scenario:

Future<dynamic> trySomething(Future<dynamic> Function() callback) async {
  doSomeInitialWork();

  try {
    return await callback();
  } catch (err) {
    throw err;
  } finally {
    doSomeCleanUpWork();
  }
}

class MyClass {
  Future<List<String>> someMethodA() async {
    return await trySomething(() async {
      return await someApiCall();
    });
  }
}

That seems to work, but it looks kind of messy. I'm not sure if what I'm doing in the async/await example is appropriate.


Solution

  • Anonymous functions in Dart are rather common (unlike Python, where lambda is very restricted).

    You therefore could make a helper function that takes the unique part as a callback.

    void trySomething(void Function() body) {
      doSomeInitialWork();
    
      try {
        body();
      } catch (err) {
        throw err;
      } finally {
        doSomeCleanUpWork();
      }
    }
    
    void someMethodA() {
      trySomething(() {
        doSomething();
      });
    }
    
    void someMethodB() {
      trySomething(() {
        doSomethingElse();
      });
    }
    

    That's basically what test() from package:test (or testWidgets() from Flutter) do.


    Update for the case described in the comment: It's not much different if the methods return Futures. For example, if you start with:

    Future<List<String>> someMethodA() async {
      return await blah();
    }
    

    then you could do:

    Future<R> trySomethingAsync<R>(Future<R> Function() body) async {
      doSomeInitialWork();
    
      try {
        return await body();
      } catch (err) {
        throw err;
      } finally {
        doSomeCleanUpWork();
      }
    }
    
    Future<List<String>> someMethodA() {
      return trySomethingAsync(() async {
        return await blah();
      });
    }