Musical Instrument Digital Interface (MIDI) has been around since the early 1980’s and the basic specification has changed little since. It is a standard by which electronic musical instruments and other devices can communicate with each other. In Marshmallow (V6.0 – API 23) Android actually got some good MIDI support, and in this series of articles we’ll take a look at how we can create a MIDI controller app. For the non-musicians and those who have no interest in MIDI, do not despair there will be some custom controls we create along the way which may still be of interest. In this article we’ll take a look some useful techniques that we can use to fully leverage the power of both Kotlin and Architecture Components.



The first thing that we need to do is discover the available MIDI devices that we can select to send MIDI events to. These devices may be app supporting MIDI services which are installed locally on the device, or they may be external devices which are accessible via an external MIDI interface – typically once which connects via the USB port. We are completely agnostic of whether a MIDI device is local or external as the communicating with both is identical, which makes life much easier.

Initially we need to discover all available devices and we’ll show them in a Spinner. Then we need to monitor for new devices being discovered so that we can detect external devices being connected or disconnected and update our list accordingly. Connections to other MIDI devices are something which may need to be maintained during the entire lifecycle of our app, so the Android Architecture components will come in handy to ensure that we can easily survive device rotations without losing connections. I won’t be going in to the specifics of Architecture Components in this series, but if you are unfamiliar with them, then you may want to check this series out first.

Let’s start with a look at the MainActivity:

MainActivity.kt class MainActivity : AppCompatActivity() { private val lifecycleRegistry: LifecycleRegistry by lazyFast { LifecycleRegistry(this) } override fun getLifecycle(): LifecycleRegistry = lifecycleRegistry private val midiController: MidiController by viewModelProvider { MidiController(application) } private val deviceAdapter: DeviceAdapter by lazyFast { DeviceAdapter(this, { it.inputPortCount > 0 }) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(main_toolbar) supportActionBar?.apply { setTitle(R.string.app_name) } midiController.observeDevices(this, deviceAdapter) supportFragmentManager.beginTransaction()?.also { it.replace(R.id.main_content, Fragment.instantiate(this, MidiPad::class.java.canonicalName)) it.commit() } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) menu?.findItem(R.id.app_bar_selector)?.actionView?.apply { findViewById<Spinner>(R.id.output_selector)?.apply { adapter = deviceAdapter onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) { midiController.closeAll() } override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { deviceAdapter[position].apply { midiController.open(this) } } } } } return true } override fun onDestroy() { midiController.closeAll() super.onDestroy() } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class MainActivity : AppCompatActivity ( ) { private val lifecycleRegistry : LifecycleRegistry by lazyFast { LifecycleRegistry ( this ) } override fun getLifecycle ( ) : LifecycleRegistry = lifecycleRegistry private val midiController : MidiController by viewModelProvider { MidiController ( application ) } private val deviceAdapter : DeviceAdapter by lazyFast { DeviceAdapter ( this , { it . inputPortCount > 0 } ) } override fun onCreate ( savedInstanceState : Bundle ? ) { super . onCreate ( savedInstanceState ) setContentView ( R . layout . activity_main ) setSupportActionBar ( main_toolbar ) supportActionBar ? . apply { setTitle ( R . string . app_name ) } midiController . observeDevices ( this , deviceAdapter ) supportFragmentManager . beginTransaction ( ) ? . also { it . replace ( R . id . main_content , Fragment . instantiate ( this , MidiPad : : class . java . canonicalName ) ) it . commit ( ) } } override fun onCreateOptionsMenu ( menu : Menu ? ) : Boolean { menuInflater . inflate ( R . menu . main_menu , menu ) menu ? . findItem ( R . id . app_bar_selector ) ? . actionView ? . apply { findViewById < Spinner > ( R . id . output_selector ) ? . apply { adapter = deviceAdapter onItemSelectedListener = object : AdapterView . OnItemSelectedListener { override fun onNothingSelected ( parent : AdapterView < * > ? ) { midiController . closeAll ( ) } override fun onItemSelected ( parent : AdapterView < * > ? , view : View ? , position : Int , id : Long ) { deviceAdapter [ position ] . apply { midiController . open ( this ) } } } } } return true } override fun onDestroy ( ) { midiController . closeAll ( ) super . onDestroy ( ) } }

MidiController is the component which handles all of the MIDI functionality, and we’ll cover that in detail in the next article. For now the important thing to know is that it is a ViewModel and if we call observe on it, we will receive callbacks whenever the list of available MIDI devices changes. These callbacks will receive a list of MidiDeviceInfo objects representing each available MIDI device, and it is the deviceAdapter instance (we’ll look at this in a moment) which will handle these callbacks.

MidiDeviceInfo, as the name suggests, contains information about the available MIDI devices. In our case we are only interested in MIDI devices which will accept input connections – i.e. the will receive MIDI events from us, so we provide some filter logic to the constructor of the DeviceAdapter instance which is created on line 10 to filter out devices which will not accept input.

Much of the MainActivity code is the setup of the menu item and Spinner which will contain a list of available devices so that the user may select one to send MIDI events to. But there are a couple of interesting little tricks in here that are worthy of a little discussion.

The first trick is the use of lazyFast for our lazy object initialisation (line 9). Kotlin supports lazy object initialisation:

val object: Object by lazy { Object() } 1 val object : Object by lazy { Object ( ) }

By doing this, the Object instance will be created the first time the object propertyis accessed, and thereafter we will get normal Kotlin immutability on a val. This is useful in Android because often we cannot create object which takes a Context argument in, for example, an Activity until onCreate has been called on the Activity because it is not a valid Context until that point. By using lazy instantiation we can overcome this provided the property which need to be initialised with a Context is not access until onCreate has been called.

The one issue here is that by default lazy initialisation is thread safe, which is a bit overkill if we know that all of this will be done on the main thread. We can easily improve performance my turning off thread safety where we know that it won’t be necessary:

val object: Object by lazy(LazyThreadSafetyMode.NONE) { Object() } 1 val object : Object by lazy ( LazyThreadSafetyMode . NONE ) { Object ( ) }

But as this is something we may be doing often in android it makes sense to device a function to do this for us:

Extensions.kt fun <T> lazyFast(operation: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE) { operation() } 1 2 3 fun < T > lazyFast ( operation : ( ) - > T ) : Lazy < T > = lazy ( LazyThreadSafetyMode . NONE ) { operation ( ) }

Now we can call this instead:

val object: Object by lazyFast { Object() } 1 val object : Object by lazyFast { Object ( ) }

The next trick is how the Adapter itself is organised:

ui/DeviceAdapter.kt class DeviceAdapter(private val context: Context, private val filter: (MidiDeviceInfo) -> Boolean = { true }, private val items: MutableList<MidiDeviceInfo> = mutableListOf(), private val adapter: ArrayAdapter<String> = ArrayAdapter(context, android.R.layout.simple_spinner_item, mutableListOf())) : SpinnerAdapter by adapter, Observer<List<MidiDeviceInfo>> { init { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } override fun onChanged(updatedItems: List<MidiDeviceInfo>?) { with(items) { clear() updatedItems?.also { addAll(it.filter(filter)) } updateAdapter() } } private fun updateAdapter() = with(adapter) { clear() addAll(items.map { context.getString(R.string.device_name, it.properties.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER), it.properties.getString(MidiDeviceInfo.PROPERTY_PRODUCT)) }) } operator fun get(index: Int) = items[index] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class DeviceAdapter ( private val context : Context , private val filter : ( MidiDeviceInfo ) - > Boolean = { true } , private val items : MutableList < MidiDeviceInfo > = mutableListOf ( ) , private val adapter : ArrayAdapter < String > = ArrayAdapter ( context , android . R . layout . simple_spinner_item , mutableListOf ( ) ) ) : SpinnerAdapter by adapter , Observer < List < MidiDeviceInfo > > { init { adapter . setDropDownViewResource ( android . R . layout . simple_spinner_dropdown_item ) } override fun onChanged ( updatedItems : List < MidiDeviceInfo > ? ) { with ( items ) { clear ( ) updatedItems ? . also { addAll ( it . filter ( filter ) ) } updateAdapter ( ) } } private fun updateAdapter ( ) = with ( adapter ) { clear ( ) addAll ( items . map { context . getString ( R . string . device_name , it . properties . getString ( MidiDeviceInfo . PROPERTY_MANUFACTURER ) , it . properties . getString ( MidiDeviceInfo . PROPERTY_PRODUCT ) ) } ) } operator fun get ( index : Int ) = items [ index ] }

It implements SpinnerAdapter, but delegates this to an ArrayAdapter . It also implements Observer > to get callbacks whenever the list of detected MIDI devices changes.

onChanged() gets called whenever the list of available MIDI devices changes, and we update an internal list of MidiDeviceInfo objects which we apply the filter to – so given the filter that was passed in earlier, we’ll only store a list of devices which will handle MIDI input. Then we update the data for the ArrayAdapter where we transform each MidiDeviceInfo in to a String containing the device Manufacturer and Product names.

The get() operator enables us to look up a specific MidiDeviceInfo item from the index of an item in the Spinner, and it is this that facilitates the lookup on line 42 of MainActivity.

So what we get here a SpinnerAdapter which will automatically update its data whenever we receive a callback from the LiveData object being observed and this is all achieved using a relatively small amount of code.

The final trick we’ll look at is how we get the ViewModel instance – in this case our MidiController (lines 5-7):

MainActivity.kt private val midiController: MidiController by viewModelProvider { MidiController(application) } 5 6 7 private val midiController : MidiController by viewModelProvider { MidiController ( application ) }

We have a function named viewModelProvider :

Extensions.kt inline fun <reified VM : ViewModel> FragmentActivity.viewModelProvider(crossinline provider: () -> VM) = lazyFast { object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>) = provider() as T }.let { ViewModelProviders.of(this, it).get(VM::class.java) } } 1 2 3 4 5 6 7 8 inline fun < reified VM : ViewModel > FragmentActivity . viewModelProvider ( crossinline provider : ( ) - > VM ) = lazyFast { object : ViewModelProvider . Factory { override fun < T : ViewModel > create ( modelClass : Class < T > ) = provider ( ) as T } . let { ViewModelProviders . of ( this , it ) . get ( VM : : class . java ) } }

This looks a little scary, but what it does is actually quite straightforward. It is an extension function for FragmentActivity so can be called on any instance of that and will return an appropriate ViewModel object with all the appropriate lookups from Architecture Components to get a cached version, or create a new instance if one does not already exist.

The type of object we require is dictated by the caller – in this case VM will actually be MidiController (which is a subclass of ViewModel). The caller also provides a mechanism for creating a new instance of this object in the lambda expression.

What viewModelProvider does is get an instance of the ViewModelProvider for this class whilst providing a factory which can create an instance of the requested object from the provider lambda. The ViewModelProvider will then return a cached instance or create a new instance if necessary.

All of this is done lazily so this will only occur whether the requested object is accessed for the first time.

This really does make it easy to harness the full power of ViewModel in a really easy manner, and in only a few lines of code.

So we have all of the framework in place for providing an up-to-date list of available MIDI devices but we do not yet a mechanism for retrieving them from. In the next article we will begin looking at the MIDI APIs to see how we can do this.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Related

Copyright © 2017 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.