Flutter: Background services

protium - Jun 17 '19 - - Dev Community

So, it's time for me to actually try Flutter. I've watched the last dev conferences and I'm amazed of what Flutter is able to do and the future Flutter (Web, Windows, Linux). If you want to take a look to that conference, here the youtube link.

Background

I never try something with a "hello world", I just go with the "heavy" stuff so I can see the power, cons and pros of the thing I'm trying. So, I grabbed an idea from my "millionaire ideas" list and started my journey.

The app: Rest

I'll build a really simple app that for me is super useful. I have tinnitus produced by Menier's Disease (BTW if you also have that disease, please leave a comment), because of that some times I like to hear some background music on my phone when I go to sleep. But I don't want to leave the phone playing music all night long.

Researching

Every time I have an awesome idea I google it. I don't want to reinvent the wheel. This time I found an application that does the job. The problem is that it tends to hang or freeze really often. Sometimes the app renders all its buttons really bad. So I decided to continue by making a simple and functional app.

Let's start

Before any code, I warn you: I'm as new in Flutter as you may be. Please do your own research. These series of post it's really awesome

Continue reading once you have the basic knowledge of Flutter and Dart.

MVP

For my app, and its first release, I want a pretty minimalist UI and simple functionality. I decided to go with these specs:

  • The timer duration goes from 5 to 60 minutes
  • The user can add or subtract time by 5 minutes
  • It has an start/stop button

This is the result

App Service: Flutter Method Call

The app has a countdown timer. I need a service to keep that timer running and eventually turn the music off when it finishes.
But, and here is were the other app failed, when you close the app and open again, you need to know what is the elapsed time of the timer running in the service, so you can update the state and start showing the current time from there.

I already knew that I had to code an Android service, so I did a little research and got what I needed. Flutter Method Call Docs

Building the Android Service

First we need to declare the service in our android manifest (android/app/src/main/AndroidManifest.xml)



<service android:enabled="true" android:exported="true" android:name="dev.protium.rest.AppService" />


Enter fullscreen mode Exit fullscreen mode

Now we need to write the service. The service needs to fulfill these specs:

1) It should be able to bind a connection with the main activity

For that lets use a Binder



private final IBinder binder = new AppServiceBinder();
public class AppServiceBinder extends Binder {
        AppService getService() {
            return  AppService.this;
        }
    }

@Override
public IBinder onBind(Intent intent) {
     return binder;
}


Enter fullscreen mode Exit fullscreen mode

2) It should start, stop and get current seconds on demand

This code is not relevant to this post but you can see the repo at the end of the article.

3) It should pause music if any is being played

If you feel curious about how you can pause the music on and android device, this is the trick



AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
if (am.isMusicActive()) {
  long eventtime = SystemClock.uptimeMillis();
  KeyEvent downEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE, 0);
  am.dispatchMediaKeyEvent(downEvent);
  KeyEvent upEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE, 0);
  am.dispatchMediaKeyEvent(upEvent);
}


Enter fullscreen mode Exit fullscreen mode

Magic, right?

Connecting to the service

At this point you've may noticed that the communication goes like this



Android Service <-> Android Activity <-> Flutter App


Enter fullscreen mode Exit fullscreen mode

Connecting from the Activity

This is pretty straightforward



private void connectToService() {
        if (!serviceConnected) {
            Intent service = new Intent(this, AppService.class);
            startService(service);
            bindService(service, connection, Context.BIND_AUTO_CREATE);
        } else {
            Log.i(TAG, "Service already connected");
            if (keepResult != null) {
                keepResult.success(null);
                keepResult = null;
            }
        }
    }

private ServiceConnection connection = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName className,
                                       IBinder service) {
            AppService.AppServiceBinder binder = (AppService.AppServiceBinder) service;
            appService = binder.getService();
            serviceConnected = true;
            Log.i(TAG, "Service connected");
            if (keepResult != null) {
                keepResult.success(null);
                keepResult = null;
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            serviceConnected = false;
            Log.i(TAG, "Service disconnected");
        }
    };


Enter fullscreen mode Exit fullscreen mode

Did you notice that keepResult variable? More on that later.

Connecting Activity to Flutter



static final String CHANNEL = "dev.protium.rest/service";
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);

        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(this::onMethodCall);
    }


Enter fullscreen mode Exit fullscreen mode

I decided to implement the interface on the activity itself so you have to change the class declaration



public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler {


Enter fullscreen mode Exit fullscreen mode

And now the implementation



@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        try {
            if (call.method.equals("connect")) {
                connectToService();
                keepResult = result;
            } else if (serviceConnected) {
                if (call.method.equals("start")) {
                    appService.startTimer(call.argument("duration"));
                    result.success(null);
                } else if (call.method.equals("stop")) {
                    appService.stopTimer();
                    result.success(null);
                } else if (call.method.equals("getCurrentSeconds")) {
                    int sec = appService.getCurrentSeconds();
                    result.success(sec);
                }
            } else {
                result.error(null, "App not connected to service", null);
            }
        } catch (Exception e) {
            result.error(null, e.getMessage(), null);
        }
    }


Enter fullscreen mode Exit fullscreen mode

We are keeping the MethodChannel.Result in the variable keepResult. Why? When we bind the service with the service connection, we will get the onServiceConnected listener called. There we know we are connected to the service. The flutter app will wait for this callback to success or fail. Don't forget to call it.

Connecting Flutter to Activity

First we need to import packages in lib/main.dart



import 'dart:async'
import 'package:flutter/services.dart';


Enter fullscreen mode Exit fullscreen mode

Inside our state widget we need this



static const MethodChannel platform = 
MethodChannel('dev.protium.rest/service');

Future<void> connectToService() async {
    try {
      await platform.invokeMethod<void>('connect');
      print('Connected to service');
    } on Exception catch (e) {
      print(e.toString());
    }
}



Enter fullscreen mode Exit fullscreen mode

Note: the channel is the same as in the MainActivity, as well as the method names. Sounds familiar? If you've developed some Apache Cordova plugin, this should be really familiar to you.

Now we are done with this 3-components-connection thing. Once the flutter app is connected it can call the start/stop methods or get the current seconds from the service timer.



final int serviceCurrentSeconds = await getServiceCurrentSeconds();
setState(() {
    _currentSeconds = serviceCurrentSeconds;
});


Enter fullscreen mode Exit fullscreen mode

Mission accomplished.

What I've learned from Flutter

As I mentioned early, I don't like "hello wold" apps. This app is pretty simple but it has some tricks to be done. I think I'm in love with Flutter and Dart's type inference. This whole process took me exactly 2 days, I'm not boasting. I sit down with my laptop yesterday. I finished the app at night. Today I've published it on the Google Play Store and wrote this article. (BTW First time publishing an app)

I've learned a lot of tricks, to list some:

  • How to run widget tests without calling the service
  • How to run test drive by setting the app's permissions first
  • How important and useful is flutter analyze You can see all of that in the repo

    GitHub logo protiumx / rest

    Turn music off after a period off time and rest

    rest

    NOTICE: This repo is not being maintained.

    A simple app that will pause music after a period of time. This app was developed with Flutter

    Install from Google Play

    Get it on Google Play

    Development

    Dependencies:

    • Android SDK
    • Android Studio
    • Flutter SDK

    Check your dependencies with flutter doctor

    Testing

    flutter test
    flutter drive --target=test_driver/home_page.dart 
    

    Running

    flutter run

    Screenshots

    TODOs

    • Add sticky notification
    • Show toast message when music is paused

    Donate

    If you liked this app feel free to buy me a coffee

    Buy Me A Coffee

    © Brian Mayo - 2019



I'll post more articles about other tricks I've learned with this little project. I encourage you to grab some idea from your personal "millionaire ideas" list and make one with Flutter.

Conclusion

I'm happy that it took me some minutes to get a nice UI without much of a pain. And I'm really eager to play around with Flutter Web and Desktop. I see so much potential on this that I told the CTO of the company I work for that we must migrate some products to flutter. The benefits are high since we have multi-platform apps (TV, smartphones, desktop, anything with a webview).

Give a try to the app and feel free to contribute with more features. I plan to add more features but for now I'm okay to have the basic functionality.
And If you want to compile and publish it for iOS, please let me know.

. . . . . . . . . . . . . . . . . . . . . . . . . . . .