Can't create handler inside thread that has not called Looper.prepare()

TBMSamTBMSam Member ✭✭
edited January 9 in Xamarin.Android

Hello everyone,

I am trying to update a TextView when the data it displays change:

var thread = new Thread(() => MethodToRun());
thread.Start();

[...]

void MethodToRun()
    {
        while (true)
        {
            Thread.Sleep(1000);
            var a = new Activity();
            a.RunOnUiThread(() => {

                int number = Get_Number();
                if (number == 1)
                {
                    v.FindViewById<TextView>(Resource.Id.textView).Text = "1";
                }
                if (number != 1)
                {
                    v.FindViewById<TextView>(Resource.Id.textView).Text = "Not 1 anymore";
                }
            });
        }
    }

Unfortunately, I am getting a runtime error in the line var a = new Activity(); saying Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

Can anyone maybe tell me what's the problem?

Thanks in advance and best regards

Answers

  • jezhjezh Member, Xamarin Team Xamurai

    You should specify a specific activity instead of var a = new Activity();

    For example the current activity, you can use the following code:

     var a = this;
    

    Besides, I assume the textView is in the layout of current Activity(MainActivity), the layout code is as follows:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="aaa"
         android:id="@+id/textView"
    />
       </LinearLayout>
    

    the code of MainActivity is :

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.activity_main);
    
            var thread = new Thread(() => MethodToRun());
            thread.Start();
        }
    
    
          void MethodToRun()
        {
            while (true)
            {
                Thread.Sleep(1000);
                var a = this;
                a.RunOnUiThread(() => {
    
                    int number = 1;
                    if (number == 1)
                    {
                        FindViewById<TextView>(Resource.Id.textView).Text = "1";
                    }
                    if (number != 1)
                    {
                        FindViewById<TextView>(Resource.Id.textView).Text = "Not 1 anymore";
                    }
                });
            }
        }
    

    Above code works fine. Hope it can help you.

  • TBMSamTBMSam Member ✭✭

    Hello and thanks again for your answer and your help.

    As I am calling the method only in an ListAdapter, this in this case does not refer to an Activity. So I changed the MethodToRun to var a = new NameOfMyActivity(); a.RunOnUiThread() But this creates a new one right? it doesn't run on the current activity or does it?
    When running the code now, he get's again the runtime exception saying: Can't create handler inside thread that has not called Looper.prepare() When adding Looper.prepare() to the method and running code again, the exception is gone, but unfortunately the TextView isn't displayed as well anymore. So this doesn't work too. :-(

  • jezhjezh Member, Xamarin Team Xamurai

    @TBMSam

    You can use the Context as a parameter of the ListAdapter. For example :smile:

     class MyAdapter : BaseAdapter<string>
    {
        string[] items;
        Activity context;
    
        public MyAdapter(string[] items, Activity context)
        {
            this.items = items;
            this.context = context;
        }
    
     //   ...... 
     }
    

    and call it like this(in an activity):

           MyAdapter myAdapter;
           myAdapter =  new MyAdapter(settingItems, this);
           listView.Adapter = myAdapter;
    

    Beside,I don't know the detail of your code(such as :Where is this TextView located?), could you please post a basic demo?

  • TBMSamTBMSam Member ✭✭
    edited January 14

    The TextView is located in a special table:

    public class NameOfMyActivity : Activity
    {
        ListOfMachines list;
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.AxmlOfMyActivity);
            [...]    
            var lv = FindViewById<ListView>(Resource.Id.ListView);
            lv.Adapter = new ListAdapter(this, Resource.Layout.List, list.CurrentList, Intent.GetStringExtra("ServerIP"));
        }
    }
    
    class ListAdapter : ArrayAdapter
    {
        List<Machine> List;
        public ListAdapter(Context Context, int ListId, List<Machine> ListOfMachines, string serverip) : base(Context, ListId, ListOfMachines)
        { 
            this.List = ListOfMachines; 
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            View v = convertView;
            if (v == null)
            {
                LayoutInflater inflater = (LayoutInflater) Context.GetSystemService(Context.LayoutInflaterService);
                v = inflater.Inflate(Resource.Layout.List, parent, false);
            }
            v.FindViewById<TextView>(Resource.Id.MachineNumberTextView).Text = List[position].Key;
            v.FindViewById<TextView>(Resource.Id.MachineNameTextView).Text = List[position].Desc;
    
            int state = Get_State(List[position].IpAddress);
            TextView tv = v.FindViewById<TextView>(Resource.Id.MachineStateTextView);//See?^^
            tv.Text = (state == 1 ? "Online" : "Offline");
        }
    }
    

    The table contains a list of machines:

    public class Machine
    {
        public long Key { get; set; }
        public string Desc { get; set; }
        public string IpAddress { get; set; }
    
        public Geraet(long key, string desc, string ipaddress)
        {
            this.Key = key;
            this.Desc = desc;
            this.IpAddress = ipaddress;
        }
    }
    
    class ListOfMachines
    {
        private List<Machine> list = null;
        private string serverip;
    
        public ListOfMachines(string ip)
        {
            this.list = GetMachinesFromWebservice();
            this.serverip = ip;
        }
        public List<Machine> CurrentList
        {
            get
            {
                if (list != null)
                {
                    return list;
                }
                list = GetMachinesFromWebservice();
                return list;
            }
        }
    }
    

    The list of machines is filled by an external WebService:

        private List<Machine> GetMachinesFromWebservice()
        {
            List<Machine> content = new List<Machine>();
    
            var request = HttpWebRequest.Create(string.Format(Constants.MachineListServicePath, serverip, Constants.WebservicePort));
            request.ContentType = "application/json";
            request.Method = "GET";
    
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var json = reader.ReadToEnd();
    
                    content = JsonConvert.DeserializeObject<List<Machine>>(json);
                }
            }
            return content;
        }
    

    The .axml for this looks like the following:

    Resource.Layout.AxmlOfMyActivity:

    <LinearLayout>
    <!-- [...] -->
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ListView" />
    </LinearLayout>
    

    Resource.Layout.List:

    <?xml version="1.0" encoding="utf-8"?> 
    <RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"     
    android:orientation="horizontal"     
    android:layout_width="fill_parent"     
    android:layout_height="fill_parent">
    
        <TextView         
        android:id="@+id/MachineNumberTextView" />
        <TextView         
        android:id="@+id/MachineNameTextView" />
        <TextView         
        android:id="@+id/MachineStateTextView" />
    

    For the sake of completeness: The following function asks a machine about its state:

        private int Get_State(string IpOfMachine)
        {
            var request = HttpWebRequest.Create(string.Format(Constants.GeraetestatusPath, ServerIp, Constants.WebservicePort, IpOfMachine));
            request.ContentType = "application/json";
            request.Method = "GET";
    
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var content = reader.ReadToEnd();
    
                    var result = System.Text.RegularExpressions.Regex.Replace(content, @"[^0-9]", "");
    
                    if (result == string.Empty)
                    {
                        return -2;
                    }
                    else
                    {
                        return Int32.Parse(result);
                    }
                }
            }
        }
    

    So the goal, or what I am trying to do, is finally getting the following Output:

    Machine Number (Key) Description / Name Online?
    123456789 "machine number 1" no
    987654321 "machine number 2" yes
    123123123 "machine number 3" no

    Hope you can get a basic idea of app functionality through this.

    Thank you again so much for your helping efforts.

    Best regards

  • jezhjezh Member, Xamarin Team Xamurai

    For the Context , you can use like this

      class ListAdapter : ArrayAdapter
    {
     List<Machine> mList;
     Context context; //** 1. define a var context **
    public ListAdapter(Context Context, int ListId, List<Machine> ListOfMachines, string serverip) : base(Context, ListId, ListOfMachines)
    { 
        this.mList= ListOfMachines; 
        this.context = context;  //**2.  init the context**
    }
    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        View v = convertView;
        if (v == null)
        {   // **3.  use context instead of Context**
            LayoutInflater inflater = (LayoutInflater) context.GetSystemService(Context.LayoutInflaterService); 
            v = inflater.Inflate(Resource.Layout.List, parent, false);
        }
       // ...............................
       }
     }
    

    In the NameOfMyActivity

        var lv = FindViewById<ListView>(Resource.Id.ListView);
        lv.Adapter = new ListAdapter(this, Resource.Layout.List, list.CurrentList, Intent.GetStringExtra("ServerIP"));
    

    Note:
    1. you should define a different var instead of List,Since the List is a class Type

               List<Machine> mList; //  
    

    2. Since this line of code is very time consuming, so why not wait until the data is updated before refreshing the Adapter

         int state = Get_State(List[position].IpAddress);
    

    Note: you can add a field within the class Machine

           public class Machine
     {
        public long Key { get; set; }
        public string Desc { get; set; }
        public string IpAddress { get; set; }
        public int state { get; set; }  //  a new field
    
      //.........................
    }
    

    In the NameOfMyActivity

                 if (ListOfMachines!=null && ListOfMachines.Count>0) {
                int length = ListOfMachines.Count;
                for (int i=0;i<length;i++)
                {
                    Machine machine = ListOfMachines[i];
                    int state = Get_State(machine.IpAddress);
                    machine.state = state;
                }
            }
    
            // then in the adapter like this
            int state = List[position].state ;
            TextView tv = v.FindViewById<TextView>(Resource.Id.MachineStateTextView);
            tv.Text = (state == 1 ? "Online" : "Offline");
    

    Hope it can help you.

Sign In or Register to comment.