Cómo programar fuera del hilo principal de la interfaz de usuario

No sé si existe un subproceso de interfaz de usuario por defecto, cómo crearlo si no, o cómo sabercuando estoy "en" donde comienza y termina si es así. ¿Cómo me aseguroPacketListener no es parte del hilo principal de la interfaz de usuario?

Sigo corriendo hacia estoNetworkOnMainThreadException. Entiendo que eso significa que estoy tratando de hacer algo en el hilo principal de la interfaz de usuario que debería tener su propio hilo separado. Sin embargo, no puedo encontrar mucho que decirme CÓMO hacer eso realmente.

Pensé que la clase MainActivity SERÍA el hilo de la interfaz de usuario, ya que, bueno, tenía una interfaz de usuario y funcionó. Sin embargo, he creado una clase separada que implementa Runnable, y puse las cosas de conexión de red allí, y todavía obtengo la excepción, por lo que todavía es parte del hilo principal, ¿sí? Puedo ver que MainActivity no extiende Thread, por lo que tal vez fue una suposición tonta. No tengo la sensación de que algo cambiará si agrego otra clase que implemente Runnable / extender Thread. Lo hará?

MainActivity.java

public class MainActivity extends AppCompatActivity {

    ArrayList<String> listItems;
    ArrayAdapter<String> adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listItems = new ArrayList<>();
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listItems);
        ListView listview = (ListView) findViewById(R.id.listview);
        listview.setAdapter(adapter);

        (new Thread(new PacketListener(adapter))).run();
    }
}

PackageListener.java:

public class PacketListener implements Runnable {
    ArrayAdapter<String> adapter;

    public PacketListener(ArrayAdapter<String> adapter) {
        this.adapter = adapter;
    }

    public void run() {
        byte[] buf = new byte[256];
        DatagramPacket packet = new DatagramPacket(buf, buf.length);

        try {
            DatagramSocket socket = new DatagramSocket();
            socket.receive(packet);
        }
        catch(Exception e) {
            adapter.add("Exception: " + e.toString() );
            e.printStackTrace();
        }
        catch(Error e) {
            adapter.add("Unspecified Error: " + e.toString() );
            e.printStackTrace();
        }
    }
}
EDITAR:

Mi último intento, tratando de seguiresta página ejemplo, ha seguido la ruta de la mayoría de mis intentos y rompió la aplicación por completo.SendAnother es un método de prueba vinculado a un botón:

public void sendAnother(View view) {
    adapter.add("button clicked");

    new Thread(new Runnable() {
        public void run() {
            listview.post(new Runnable() {
                public void run() {
                    adapter.add("inside worker thread");
                }
            });
        }
    });
}

Creo que luego intentaré finalmenteAsyncTask. Ya no me importa mucho si está "sucio".

Respuestas a la pregunta(3)

Su respuesta a la pregunta