22 Mayıs 2013 Çarşamba

Basit bir Android Twitter İstemcisi

Merhaba,

Bugün Android'te twitter arama api*'sini kullanarak basit bir uygulama yazacağız. Yazacağımız uygulama başlangıç seviyesindekiler için çok uygun olmayabilir fakat yine baştan sona bu makale takip edilirse meraklılar istedikleri sonuca ulaşabilirler. Uygulama geliştirmeye geçmeden önce yapacağımız uygulamanın neye benzediği hakkında bir fikir edinmeniz açısından tamamlanmış uygulamanın ekran birkaç ekran çıktısını paylaşmakta fayda görüyorum.

 Yan tarafta uygulama açıldığında karşınıza çıkan ekran görünmektedir. Üst tarafta bir arama alanı ve buton, ve mevcut butona bastığınızda ilgili aramanızdan sonuçlar gelmektedir.


Bu resimde ise uygulama içerisinde 'mehmet' araması için gelen sonuçlar bir listede gösterilmektedir.

















Uygulama arayüzü basit ve sadedir. Makalede stillere ve görsel arayüz detaylarına fazla değinilmeyecektir. Dilerseniz daha sonra bu uygulamayı bir prototip gibi kullanıp kendi uygulamanızda farklı stiller uygulamak şeklinde kullanabilirsiniz.

Uygulamanın kaynak kodlarını  buradan indirebilirsiniz

Bu makalede hangi Android özelliklerini nedenleriyle birlikte öğreneceğinizi belirtmekte fayda görüyorum

Kullanılan Özellikler

Fragment: Klasik Activity tabanlı ve fragment kullanılmadan uygulama geliştirme tekniği artık geride kaldığı ve fragmentlerin uygulama geliştirme aşamasında daha modüler çalışmanıza olanak tanıdığı için fragment kullanmayı uygun gördüm. Aslında yazacağımız uygulama Fragment'lere ihtiyaç duyan bir uygulama değil, sadece nasıl kullanıldığını göstermek açısından fragment kullandım. Bilmeyenler için şunu belirtelim: Fragment kavramı Android API seviyesi 11'de geldi, fakat Android Support Library sayesinde daha düşük seviyedeki API'ler de destekleniyor.Eğer Eclipse ve ADT'nin(Android Developer tools) son versiyonunu kullanıyorsanız oluşturacağınız projede Support Library otomatik olarak gelecektir. Kullanacağımız fragmentler de Support Library fragmentleri olacaktır. Fragment, bir Activity'yi oluşturan parçalar olarak tanımlanabilir, ancak Activity'den bağımsız olarak.

ListView Kullanımı, Performans ve Yavaş yükleme: ListView, Android'te en çok kullanılan widget'lardan biridir ve eğer doğru kullanılmazsa birçok performans problemine sebep olur. Bir listview'in en basit haliyle kullanımını değil, onu nasıl en performanslı biçimde kullanacağımızı öğreneceğiz. Yavaş yükleme ise yine resimlerin liste hareket ettirildiğinde takılmaması için gerekli performans optimizasyonu da yapılmıştır.

Asynctask Kullanımı: Mobil dünyasında multithreading kavramı tabiri caizse olmazsa olmazdır. Bir uygulamanın kullanıcı arayüzü işlemleri main thread'te gerçekleştirilir. Ağ işlemleri, diskten dosya yükleme gibi işlemler ağır işlemlerdir ve eğer bunlar da main thread'te gerçekleştirilmeye çalışılırsa uygulamanın cevap verme süresi son kullanıcılar açısından felaket olur. Çokça karşılaşılan ANR(Activity not responding, uygulama cevap vermiyor) hatasının sebebi ağır işlerin main thread'te gerçekleştirilmeye çalışılmasıdır. Aslında ağ işlemlerini API seviyesi 11 ve üzeri cihazlarda gerçekleştiremiyorsunuz, tabi ki aksini belirtmedikçe (Thread policy seviyesini aşağıya çekip bunu yapabilirsiniz). Tüm bu saydıklarımızı baz alarak Android'in multithreading işlemleri için yazdığı AsyncTask sınıfını kullanacağız.

Twitter APIsi ve JSON parse etme: Bu uygulamada basit de olsa twitter API'sini kullancağız ve Android'te nasıl JSON parse edildiğini öğreneceğiz.


Bilmeniz Gerekenler

Bu makalede anlatılan uygulamayı anlayarak geliştirmek istiyorsanız şunları bilmelisiniz.

  • Nesne Tabanlı Programlama
  • Java Programlama Dili
  • Temel Seviyede Android Uygulama Geliştirme 
Eğer bunları bilmiyorsanız bu makaleden yeterince faydalanmayabilirsiniz.

Başlayalım

Yeni Android Uygulama Projesi Oluşturma

Uygulamamızı Eclipse ortamında geliştireceğiz, Yeni uygulama oluşturmak için şu adımları takip etmemiz gerekir:

1-Eclipse üzerinden File-New-Android Application Project denilir ve bir proje ismi girilir. package name, minimum sdk, target sdk alanları seçilir ve next'e tıklanır


 2-Projenin Workspace'i seçilir, Mevcut workspace'iniz varsa orda kalmanızı tavsiye ederim. Seçili ve seçili olmayan kutucuklar varsayılan olarak kalabilirler



3-Eğer uygulama için bir ikon seçmek istiyorsanız bu adımda seçebilirsiniz. çeşitli ekran derinliklerine göre farklı çözünürlükteki ikonları ekleme şansınız var.




4-Bir sonraki pencerede varsayılan olarak blank activity'yi seçili olarak bırakabilirsiniz




5-Son aşamada Activity ve activity için kullanacağınız xml layout'un ismini belirleyebilirsiniz. Ben olduğu gibi bıraktım. Siz istediğinizi girebilirsiniz.





Evet, projemizi oluşturduktan sonra projemizi tasarlamaya başlayabiliriz.

Uygulama Tasarımı

Mobil uygulama geliştirmede tasarım hayati bir öneme sahiptir. Platform bileşenlerini düzgün yerleştirmek, aralarındaki ilişkileri düzenlemek için mutlaka düzgün bir nesne tasarımı yapılmalıdır. Bizim uygulmada kullanacağımız sınıf diyagramı aşağıdaki gibi olacaktır

Sınıf Diyagramı


Diyagramda belirtilen tüm sınıflar açıklanacaktır. Fakat başlangıçta XML layout'ları düzgün olarak tanımlanmalıdır.

Uygulamamız tek bir Activity'den oluşmaktadır ve bu activity de biri üstte olmak üzere (topfragment) biri de altta ve listeyi içeren (mainfragment) olmak üzere iki fragment vardır.

Aşağıda topfragmentlayout.xml dosyası verilmiştir

 <?xml version="1.0" encoding="utf-8"?>  
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" >  
   
   <EditText  
     android:id="@+id/search_input"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:layout_alignParentLeft="true"  
     android:layout_alignParentTop="true"  
     android:layout_marginLeft="20dp"  
     android:layout_marginTop="15dp"  
     android:ems="10"  
     android:singleLine="true"  
     android:hint="@string/search_hint" >  
   
     <requestFocus />  
   </EditText>  
   
   <Button  
     android:id="@+id/search_button"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignBaseline="@+id/search_input"  
     android:layout_alignBottom="@+id/search_input"  
     android:layout_alignParentRight="true"  
     android:text="@string/search_text" />  
   
 </RelativeLayout>  


Top fragment üstte kullanacağımız bir kutucuk(edittext) bir de buton'dan oluşan basit bir yapıdır. Bu fragment'in layout'unu değerlendirmek için bir Fragment sınıfı yazmamız gerekmektedir. Daha sonra bu fragment sınıfını Ana activity layout'u için kullanabilelim.

TopFragment.java kodu aşağıda verilmiştir

 public class TopFragment extends Fragment implements View.OnClickListener{  
 public static final String twitterSearchUrl="http://search.twitter.com/search.json?q=";  
 EditText searchText;  
      @Override  
      public View onCreateView(LayoutInflater inflater, ViewGroup container,  
                Bundle savedInstanceState) {  
           View topView=inflater.inflate(R.layout.topfragmentlayout, container, false);  
        searchText=(EditText)topView.findViewById(R.id.search_input);  
           Button button=(Button)topView.findViewById(R.id.search_button);  
           button.setOnClickListener(this);  
           return topView;  
      }  
      @Override  
      public void onClick(View v) {  
           MainActivity activity=(MainActivity)getActivity();  
           activity.onSearchButtonClicked(searchText.getText().toString());  
           //hide keyboard  
           InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(  
                   Context.INPUT_METHOD_SERVICE);  
                imm.hideSoftInputFromWindow(searchText.getWindowToken(), 0);  
             
      }  
   
 }  


Yukarıdaki Fragment kodunu inceleyelim
 public static final String twitterSearchUrl="http://search.twitter.com/search.json?q=";   

satırı kullanacağımız Twitter API URL'ini belirtiyor, daha sonra aradığımız sözcüğü bu url'in sonuna ekleme yaparak kullanacağız.
 @Override   
    public View onCreateView(LayoutInflater inflater, ViewGroup container,   
         Bundle savedInstanceState) {   


onCreateView metodu da diğer Android bileşenlerinde olduğu gibi bir callback metodudur. Callback metodları daha alt düzeyden koda daha üst düzeyde ulaşmayı sağlayan metodlardır. Daha fazla merak ediyorsanız buraya bakabilirsiniz. Bu metod içerisinde xml layout'unu inflater ile inflate edebilirsiniz.

Not: Inflate etmek demek xml kaynağını parçalayıp kullanılabilir java koduna dönüştürmek demek, Türkçe'de şişirmek gibi anlamları var.

Layout'unuzu inflate ettikten sonra içerisinde tanımlı buton, kutocuk gibi View'lara erişebilirsiniz.

Fragment'teki View.OnClickListener arayüzü fragment'teki buton tıklandığında aksiyon alınması için eklenmiştir. Siz isterseniz OnClickListener kodunu içerde de yazabilirsiniz. Ancak daha esnek olmak istiyorsanız içeride değil dışarıda kullanmanızı tavsiye ederim.

Ara butonuna tıklandığında tıklandığında Activity'miz üzerinden diğer fragment olan MainFragment'e bir yükleme işleminin yapılması gerekmektedir. İki fragment birbiriyle konuşamadığına göre bunu bir şekilde activity üzerinden yapmamız gerekir.

Bir bilgi olarak içinde bulunduğumuz fragment'in kullanıldığı activity'yi çekebiliyoruz (getActivity() metoduyla). O zaman bir çözüm olarak MainFragment ve MainActivity'nin ikisinin birlikte kullanacağı bir Interface (arayüz) tanımlamak bir çözüm olabilir. Bu sayede TopFragment'te buton tıklandığında Activity'nin uygun Interface metodu tetiklenir ve bu da MainFragment'te gerekli tetiklemeyi yapar.

Kullanacağımız Interface şudur:
 public interface SearchClickListener  
 {  
   public void onSearchButtonClicked(String searchText);  
 }  

MainFragment için kullanılan Xml layout'u aşağıda verilmiştir.
 <ListView xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@android:id/list"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" >  
 </ListView>  

Gördüğünüz gibi fragment'imiz tek bir ListView'den oluşuyor. Uygulamamız için her bir liste satırı için ortak bir layout tanımlamamız gerekmektedir.

Bu layout kodu aşağıda verilmiştir:
tweetlistrow.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent" >  
   <ImageView  
     android:id="@+id/profileImage"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignParentLeft="true"  
     android:layout_alignParentTop="true"  
     android:layout_marginLeft="14dp"  
     android:layout_marginTop="14dp"  
     android:src="@drawable/ic_launcher" />  
   <TextView  
     android:id="@+id/tweetdatetext"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignBaseline="@+id/userInfoText"  
     android:layout_alignBottom="@+id/userInfoText"  
     android:layout_alignParentRight="true"  
     android:layout_marginRight="30dp"  
     android:text="Small Text"  
     android:textAppearance="?android:attr/textAppearanceSmall" />  
   <TextView  
     android:id="@+id/userInfoText"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignTop="@+id/profileImage"  
     android:layout_marginLeft="19dp"  
     android:layout_toRightOf="@+id/profileImage"  
     android:text="Medium Text"  
     android:textAppearance="?android:attr/textAppearanceMedium" />  
   <TextView  
     android:id="@+id/tweetcontenttext"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:layout_alignLeft="@+id/userInfoText"  
     android:layout_below="@+id/userInfoText"  
     android:text="TextView" />  
 </RelativeLayout>  

Bu layout'u daha sonra listemizdeki satırları oluştururken kullanacağız. Profil fotoğrafı için bir ImageView, Kullanıcı bilgisi, tweet zamanı ve içerik için ise birer TextView kullanılmıştır.

Şimdi de activity'mizde kullanacağımız layout kodunu yazma vakti geldi.

activity_main.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:paddingBottom="@dimen/activity_vertical_margin"  
   android:paddingLeft="@dimen/activity_horizontal_margin"  
   android:paddingRight="@dimen/activity_horizontal_margin"  
   android:paddingTop="@dimen/activity_vertical_margin"  
   tools:context=".MainActivity" >  
   <fragment  
     android:id="@+id/topFragment"  
     android:name="com.example.listviewsample.TopFragment"  
     android:layout_width="fill_parent"  
     android:layout_height="wrap_content"  
     android:layout_alignParentLeft="true"  
     android:layout_alignParentTop="true"  
     tools:layout="@layout/topfragmentlayout" />  
   <fragment  
     android:id="@+id/mainFragment"  
     android:name="com.example.listviewsample.MainFragment"  
     android:layout_width="fill_parent"  
     android:layout_height="fill_parent"  
     android:layout_below="@+id/topFragment"  
     android:layout_centerHorizontal="true"  
     tools:layout="@android:layout/list_content" />  
 </RelativeLayout>  


Gördüğünüz gibi, MainActivity için kullanacağımız layout iki adet fragmentten oluşuyor, MainActivity kodunda bu layout'u activity'mize contentView olarak ekleyeceğiz.

MainActivity kodu aşağıda verilmiştir.
MainActivity.java
 public class MainActivity extends FragmentActivity implements SearchClickListener{  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_main);  
      }  
      @Override  
      public void onSearchButtonClicked(String searchText) {  
           MainFragment mainFragment=(MainFragment) getSupportFragmentManager().findFragmentById(R.id.mainFragment);  
           mainFragment.onSearchButtonClicked(searchText);  
      }  
 }  

Gördüğünüz gibi activity kodumuz fragmentler sayesinde oldukça sade bir hal aldı. Yazdığımız fragment'leri layout içerisinde tanımladık ve o layout kullanılmaya başladığı andan fragment'in onCreateView metodu çalışmaya başladı. Burada dikkat edilmesi gereken bir şey var. Activity'miz mutlaka FragmentActivity'den türemeli (support library kullandığımız için), aksi halde activity'lerde fragment kullanılamayacaktır.


           MainFragment mainFragment=(MainFragment) getSupportFragmentManager().findFragmentById(R.id.mainFragment);  


bu satır ise fragmenti alıyor. Daha önce de bahsedildiği gibi Mainfragment'le ortak kullanılmak üzere bir SearchClickListener arayüzü oluşturmuştuk.

MainFragment kodumuz aşağıda verilmiştir.

MainFragment.java
 public class MainFragment extends ListFragment implements SearchClickListener{  
      ListView list;  
   ProgressDialog dialog;  
      @Override  
      public View onCreateView(LayoutInflater inflater, ViewGroup container,  
                Bundle savedInstanceState) {  
           // TODO Auto-generated method stub  
        list=(ListView)inflater.inflate(R.layout.mainfragmentlayout, container, false);  
           return list;  
      }  
      @Override  
      public void onSearchButtonClicked(String searchText) {  
     showDialog();  
           new GetTweetsTask().execute(searchText);  
      }  
      private void showDialog()  
      {  
           if(dialog==null)  
           {  
                dialog=new ProgressDialog(getActivity());  
                dialog.setTitle("Yukleniyor");  
           }  
           dialog.show();  
      }  
      private void closeDialog()  
      {  
           if(dialog!=null && dialog.isShowing())  
           {  
                dialog.dismiss();  
           }  
      }  
      private class GetTweetsTask extends AsyncTask<String, Void, ArrayList<Twit>>  
      {  
           @Override  
           protected ArrayList<Twit> doInBackground(String... params) {  
                String url=TopFragment.twitterSearchUrl+params[0];  
                TwitsParser parser=new TwitsParser(url);  
                return parser.parse();  
           }  
           @Override  
           protected void onPostExecute(ArrayList<Twit> result) {  
             list.setAdapter(new MainListAdapter(result));  
             ((MainListAdapter)list.getAdapter()).notifyDataSetChanged();  
             closeDialog();  
           }  
           @Override  
           protected void onProgressUpdate(Void... values) {  
                // TODO Auto-generated method stub  
                super.onProgressUpdate(values);  
           }  
      }  
 }  


Android görsel bileşenlerimizi neredeyse bitirdik, hatta MainFragment'tle birlikte ufaktan uygulamanın lojiğine biraz girmiş de olduk.

Dikkatinizi çektiyse MainFragment ListFragment'ten türüyor. ListFragment Fragment'in bir alt sınıfıdır ve içerik olarak bir ListView alır. ListActivity'ye aşinaysanız ListFragment size yabancı gelmeyecektir.


Bu sınıfta dikkatimizi çeken onSearchButtonClicked içerisinde başlatılan GetTweetsTask sınıfı.  Asynctask'tan daha önce bahsetmiştik, Bu generic sınıf main thread ile arka plandaki threadlerin birbirlerini kilitlemeden, senkronizasyon problemlerine neden olmadan bir arada çalışmasını sağlar. Üç adet parametre alır: Params, Progress ve Result. Daha sonra bu parametreler ilgili sınıfın metodlarında parametre olarak kullanılır. AsyncTask sınıfı ondan yeni bir sınıf türetip kendi ihtiyacınıza göre kullanmanız için tasarlanmıştır, başka bir şekilde kullanmanız tavsiye edilmez. AsyncTask'la ilgili belirtmek gereken bir şey ise sadece doInBackground metodunun arka plandaki thread'te çalışması ve arka plandaki thread'ten main thread'e müdahale edemeyeceğiniz. Sözgelimi bir butonun ismini doInBackground metodu içinde değiştiremezsiniz.


Peki GetTweetsTask ne iş yapıyor? belli bir arama sözcüğüne göre Twitter API'ye gidip oradaki twitleri almamızı sağlıyor. Bu işlemin doInBackground'ta yapılması gayet normal ve hatta gerekli. Çünkü ağ işlemi ve kullanıcıyı ilgilendirmiyor. Kullanıcıyı ilgilendiren o arada ne olduğunun kendisine bildirilmesi. Bu da task başlatılmadan önce showDialog() metoduyla sağlanmış oluyor. doInBackground'da parse işlemi yapıldıktan sonra onPostExecute(Result) metodu çalışıyor ve bu sayede main thread'te güncelleme yapılabilir oluyor.  Burada yapılan güncelleme ListView güncellemesi. Ancak buna geçmeden önce twitlerimizi nasıl parse ettiğimize bakalım. Bu işi yapması için bir TwitsParser sınıfı yazdım.

TwitsParser.java
 public class TwitsParser {  
      private String parseUrl;  
      public TwitsParser(String url)  
      {  
           parseUrl=url;  
      }  
      public ArrayList<Twit> parse(){  
           JSONArray tweetArray=null;  
           StringBuilder tweetFeedBuilder = new StringBuilder();  
     HttpClient tweetClient=new DefaultHttpClient();  
     try  
     {  
          HttpGet get=new HttpGet(parseUrl);  
          HttpResponse response=tweetClient.execute(get);  
          //check the status  
          StatusLine status=response.getStatusLine();  
          if(status.getStatusCode()==200){  
               HttpEntity entity=response.getEntity();  
               InputStream content=entity.getContent();  
               InputStreamReader isr=new InputStreamReader(content);  
               BufferedReader tweetReader=new BufferedReader(isr);  
               String line;  
               while((line=tweetReader.readLine())!=null){  
                    tweetFeedBuilder.append(line);  
               }  
               JSONObject resultObject=new JSONObject(tweetFeedBuilder.toString());  
               tweetArray=resultObject.getJSONArray("results");  
               return getTweetsFromArray(tweetArray);  
          }else  
          {//error  
          }  
     }catch(Exception e){}  
           return getTweetsFromArray(tweetArray);  
      }  
      private ArrayList<Twit> getTweetsFromArray(JSONArray tweetsArray){  
           ArrayList<Twit> tweets=new ArrayList<Twit>(tweetsArray.length());  
           for (int t=0; t<tweetsArray.length(); t++) {  
                try {  
                     JSONObject tweetObject = tweetsArray.getJSONObject(t);  
                     Twit twit=new Twit();  
                     twit.setUserInfo(tweetObject.getString("from_user"));  
                     twit.setProfileImageUrl(tweetObject.getString("profile_image_url"));  
                     twit.setDate(tweetObject.getString("created_at"));  
                     twit.setTweetContent(tweetObject.getString("text"));  
                     tweets.add(twit);  
                } catch (JSONException e) {  
                     // TODO Auto-generated catch block  
                     e.printStackTrace();  
                }  
           }  
           return tweets;  
      }  

Bu parser sınıfının üstünde fazla durmayacağım. Ama basitçe şunu söylemek gerekiyor. Bu sınıf kendisine verilen url'i Twitter API'sine göndererek gelen JSON'ı parçalayıp java nesnelerine dönüştüren bir sınıf. Bu sınıf twit listemizi almak için kullanıldı.

Twitleri aldıktan sonra yapmamız gereken o twitleri listeye yüklemek. ListView'in her bir satırı için belirlediğimiz xml layout'unu hatırlamanızı tavsiye ederim burda.

ListView ve Çalışma Yapısı

ListView, TextView gibi düz bir yapıda olmayıp bir listeyi içeren bir widget olduğundan çalışma şekli de farklıdır ve ayrı bir şekilde incelenmelidir. Bir ListView'e doğrudan item eklenip çıkarılmaz. Bir AdapterView yardımıyla her bir satır için ayrı ayrı muamele edilebilir bir hale getirilir ve AdapterView yardımıyla liste doldurulur. Burada bizim kullandığımız kendi tanımladığımız MainListAdapter. İncelememize oradan devam edebiliriz.


MainListAdapter.java
 public class MainListAdapter extends BaseAdapter{  
      private ArrayList<Twit> twits;  
      HashMap<Integer, ImageView> imageViewCollections;//to get images by tags  
      public MainListAdapter(ArrayList<Twit> data)  
      {  
           twits=data;  
           imageViewCollections=new HashMap<Integer, ImageView>();  
           new ImageLoaderTask().execute(twits);  
      }  
      @Override  
      public int getCount() {  
           // TODO Auto-generated method stub  
           return twits.size();  
      }  
      @Override  
      public Object getItem(int position) {  
           // TODO Auto-generated method stub  
           return twits.get(position);  
      }  
      @Override  
      public long getItemId(int position) {  
           // TODO Auto-generated method stub  
           return position;  
      }  
      @Override  
      public View getView(int position, View convertView, ViewGroup parent) {  
           // TODO Auto-generated method stub  
           TwitViewHolder viewholder =new TwitViewHolder();  
           if(convertView==null)  
           {  
                LayoutInflater inflater=  
                     (LayoutInflater)parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
                convertView=inflater.inflate(R.layout.tweetlistrow, parent, false);  
                viewholder.userInfoText=(TextView)convertView.findViewById(R.id.userInfoText);  
                viewholder.contentText=(TextView)convertView.findViewById(R.id.tweetcontenttext);  
                viewholder.profileImage=(ImageView)convertView.findViewById(R.id.profileImage);  
                viewholder.dateText=(TextView)convertView.findViewById(R.id.tweetdatetext);  
                imageViewCollections.put(position, viewholder.profileImage);  
                convertView.setTag(viewholder);  
           }else{  
                viewholder=(TwitViewHolder) convertView.getTag();  
                imageViewCollections.put(position, viewholder.profileImage);  
           }  
           //set view properties  
           viewholder.userInfoText.setText(twits.get(position).getUserInfo());  
           viewholder.dateText.setText(twits.get(position).getFormattedDate());  
           viewholder.contentText.setText(twits.get(position).getTweetContent());  
           return convertView;  
      }  
      private void setBitmap(Bitmap bitmap, int position)  
      {  
       ImageView imageViewRef=imageViewCollections.get(position);  
       if(imageViewRef!=null)  
    imageViewRef.setImageBitmap(bitmap);  
      }  
      private class ImageLoaderTask extends AsyncTask<ArrayList<Twit>, BitmapPositionPair, Boolean>  
      {  
           @Override  
           protected Boolean doInBackground(ArrayList<Twit>... params) {  
                ArrayList<Twit> twits=params[0];  
                for(int i=0;i<twits.size();i++){  
                     Bitmap bitmap=ImageHelper.getBitmapFromURL(twits.get(i).getProfileImageUrl());  
                     BitmapPositionPair bpp=new BitmapPositionPair();  
                     bpp.setBitmap(bitmap);  
                     bpp.setPosition(i);  
                     publishProgress(bpp);  
                }  
                return null;  
           }  
           @Override  
           protected void onPostExecute(Boolean result) {  
                // TODO Auto-generated method stub  
                super.onPostExecute(result);  
           }  
           @Override  
           protected void onProgressUpdate(BitmapPositionPair... values) {  
                setBitmap(values[0].getBitmap(), values[0].getPosition());  
           }  
      }  
      class BitmapPositionPair  
      {  
           private Bitmap bitmap;  
           private int position;  
           public Bitmap getBitmap() {  
                return bitmap;  
           }  
           public void setBitmap(Bitmap bitmap) {  
                this.bitmap = bitmap;  
           }  
           public int getPosition() {  
                return position;  
           }  
           public void setPosition(int position) {  
                this.position = position;  
           }  
      }  
      static class TwitViewHolder   
      {  
           ImageView profileImage;  
           TextView userInfoText;  
           TextView dateText;  
           TextView contentText;  
      }  
 }  


Bu sınıfı da yazdıktan sonra uygulamamızı bitirme noktasına geliyoruz nerdeyse. Fakat burada incelememiz gereken nazik durumlar var. Adapter arayüzünün bize sağladıklarından iyi faydalanamazsak uygulamalarımızdaki ListView'in doğasına aykırı olarak akmaması veya çok zor akması ve takılması gibi durumlarla karşılaşabiliriz.

Bunun için önünüze iki tane durum koymak istiyorum, siz de nasıl yapacağınızı düşünün bu arada. Diyelim ki elimizde 200 satırlık bir liste verisi var ve bunların içerdiği kullanıcı arayüzü bileşenlerinden her biri her bir liste satırı yaratıldığında tekrar tekrar yaratılıyor. Her liste satırında 4 tane bileşen(View) olduğunu düşündüğünüzden yukardan aşağıya her scroll'da 4*200=800 tane yaratım oluyor. Bu bir Android cihazın hafızası açısından felaket bir durumdur. Buradaki optimizasyonu birazdan yapacağız, ama önce önünüze diğer durumu koymak istiyorum.


Diyelim ki (bizim yaptığımız gibi) ağ üzerinden resim çekeceksiniz (hem de liste halinde). Bu resimleri bir listeye main thread içerisinde yüklemeye kalkarsanız listeyi scroll edebilir misiniz? Ben cevap vereyim: Bekleyen resimler yüklenmeden scroll yapamazsınız, bu ise ListView için, uygulama için ve hatta insanlık için büyük bir felakettir. Bunun da çözümünü bulacağız.


 public View getView(int position, View convertView, ViewGroup parent) 


Adapter içindeki bu metoda dikkatinizi çekmek istiyorum. Listenin satırlarının oluşumunu bu metod kontrol ediyor (Diğer metodları inceleyebilirsiniz). burada position satırın pozisyonu yani indeksi, convertView eğer varsa geri dönüştürülen bir View'i ve parent ise bu view'in ekleneceği içeren view i belirtiyor.

İlk durumdaki o performanssız duruma hiç convertView'i kullanmasaydık erişebilirdik. getView metodunda her bir view yaratıldığında ilgili pozisyondaki view bir recycler'a(geri dönüştürücüye) atılır ve daha sonra aynı pozisyondaki view'i elde etmek için o objeyi tekrar yaratmak yerine recycler'dan geri alınır. Ancak bu durum bile bizim için performanslı bir durum sayılamaz. Çünkü yine 400 item için maksimum bir 400 tane item yaratılacak. Burada avantaj daha önce scroll olan pozisyonlara geri dönüldüğünde ordaki view'ların recycler'dan geri dönmesi.

ViewHolder Pattern

Bizim durumumuzda her satır için üç view'lik bir konteynır'ımız var. Buna bir satırın view tipi denilebilir. Statik bir sınıf oluşturup özelliklerinden arındırılmış widget'larımızı koyarsak tekrar tekrar bu sınıfı kullanabiliriz. Bu bir liste için çok iyi bir optimizasyondur.
Bu sayede her bir view tipiniz için memory'de bir classlık yer allocate edilmesini sağlarsınız.


Resim Optimizasyonu ve Yavaş yükleme

Ağdan yüklenen resimlerin main thread'ten yapılmaması gerektiğini söylemiştik. Bu yüzden diğer içeriğin aksine resimleri ayrı bir thread içerisinde (Asynctask-ImageLoaderTask) içerisinde gerçekleştiriyoruz. Bu sayede lazy loading denen durumu elde ediyoruz.


Bu makaleye başlarken bu kadar süreceğini tahmin etmediğim için son konuları biraz atlamış oldum. Uygulamanın kaynak kodlarını  buradan indirebilirsiniz. Eclipse ortamına proje olarak import edip hemen kullanabilirsiniz. Lütfen sorularınız, düzeltmek istediğiniz bir şey varsa yorum yapmaktan çekinmeyiniz.

Mutlu programlamalar!




1 yorum: