scrcpyui - macOS UI app to use scrcpy
You should be ready with this in 10 minutes
After our first post about kotlin native, we’re back and this time we’ll talk about implementing a macOS app that will be the UI connection with the command line tool scrcpy, this tool is an easy way to mirror your android device in your computer. Long time ago I thought that would be great to have a mac status bar app that can allow users to easily execute the tool, this status bap app will automatically recognize any device connected (using adb) and just launch the tool when the user clicks on the desired device.
Since it turned out to be a really simple an funny application I decided to open source it, since every project has to have a nice logo, I created this one after a few iterations with Gimp. Believe it or not, but my inspiration was the anarchy symbol.
First iteration
In this iteration I just wanted to be sure that all the pieces can work together and that we can distribute the app without any problem. Summing up a list with all the things our app has to have to be fully functional:
- Get devices connected from adb
- Parse response from adb and transform it in an array of devices
- Draw the list of devices in the status
- Open scrcpy once user clicks on a device
To do that we need to execute another app and parse their output, we’re going to do so using Process from swift. Here how the method looked like in the first version:
This method is executing a command, redirecting the standard output to a pipe that we read until it’s finished. The readOutput
parameter is going to avoid connecting to the pipe if we’re not really interested in the output, which is the case of launching the scrcpy command.
After some hacking I got the first draft of the app, all the code was in the AppDelegate as you can see in this commit).
A bit of feedback
Once the first version was implemented I wanted to get quick feedback on things I can improve, decided to share the app with one of my teammates and the first thing he pointed out was that the app was not refreshing the list of devices when opening it, so you have to do that manually everytime to connect a new device. First thing that came to my mind was the macOS wifi app, when you click on it, wifi networks are refreshed. You also get an status update on the first item of the menu:
Wanted to do it properly, so I decided to split up my logic into different components, and write some unit testing to verify that the logic is properly implemented. Finally the easier pattern to implement was model-view-presenter. Basically the presenter will take the actions user does with the status bar, execute something and draw back again the desired status in the UI.
-
DeviceRepository: Class that will basically receive in the constructor an instance of CommandLineProtocol to retrieve the devices from the console and parse it into strings. Will call to adb, as explained in the intro.
-
CommandLineProtocol: Protocol that represents a command line interaction with only one method.
-
BashCommandLine: Implementation of the CommandLineProtocol that launches a command as a process and reads the output.
-
StatusBarProtocol: UI Protocol than represents the status bar and some simple actions.
- ScrCpyPresenter: Class that interacts with all the previous classes/protocols. Receives all of them and connects them with the business logic.
Our main mission after these changes was to leave our AppDelegate with less code and just UI code, and a few part of presenter calls. Here a small arch representation:
You said testing, right?
One of the reasons of such refactor was to enable the code to be more testable. Allowing us to inject some mocks of the protocols and to verify that the logic inside of the components looks good.
The first logic we wanted to verify was how to obtain the devices from adb and how to parse them in way they can be used in the next step, we created the DevicesRepositoryTest class that basically will receive some mock implementation of the CommandLine tool that simulates an empty response as well as one device response.
For all of our tests we decided to use XCTest without any framework to mock or verify. All mocks are implemented manually and we implemented a really rudimentary system to verify some interaction with some methods.
As you can see, we’re only adding the name of the method to a list to verify later that has been called, is not optimal, since we’re here not keeping track of important things like order of calling or paremeters with some matchers. Since I’m not an expert on mocks/stub frameworks in swift and after a first research all of them seemed to be very heavy I decided to go that way. Feel free to contact me if you know any lightweight framework to use.
Testing is important, so is executing tests everytime you do a change. With GitHub actions you can do that easily, we’ve created two flows:
- Pull requests that are pointing to main. In this case we juest execute tests. You can take a look to the yml file for the action.
- Code that arrives to main, in this case we’re not only running tests but also building and bundling the binary to later distribute it, here the action.
Distributing the app
Once the app is completed the next challenge to solve was how to distribute the binary. Here is when homebrew comes to the rescue. It was not only about distributing the binary but also to makes sure all the dependencies the app needs are installed in the system, and also homebrew is a great tool for that. Since it’s an already bundled macOS app we have to distribute it using cask. In order to do that you need to create your own homebrew repo to later tap on it.
By executing this command you’ll generate in your local taps an empty repository that later you can sync with your github account. And then you can start creating your formulaes or casks in it.
In this case we’re doing so for the scrcpyui app, as well as defining the dependency with scrcpy.
After this is pretty easy to tap into your custom tap to install and update the app, you just need to do this:
Please, give it a try and feel free to distribute, contribute or just provide a bit of feedback.