Android 401Başlangıç seviyesi derslerde Android uygulama geliştirme ortamını detaylı olarak inceliyoruz.

Tüm Eğitimleri Aldın mı? Kendini sınamanın tam zamanı

Haydi Sınava Gir

Reader Uygulaması

Kategori : | Lisans : Creative Commons (by-nc-sa) | En son 11.05.2016 tarihinde güncellendi
Eğitmen : Geleceği Yazanlar Ekibi
Etiketler : android reader uygulaması

Bu bölümde sizlere bir RSS kaynağından gelen haberleri okuyarak listeleyen ve listedeki bir habere tıkladığınızda orjinal kaynağa giden bir uygulama hazırlayacağız. Yazıyı okumaya başlamadan önce aşağıdaki konuları okumanızı tavsiye ederiz;

Bu bölümde yapacağımız uygulama, aslında herhangi bir RSS kaynağını alıp bir ListView içerisine dolduran bir uygulama olacak. ListView içerisinde yer alan herhangi bir satıra tıklandığında haberin detay sayfası bir web tarayıcı içerisinde açılacak. Uygulamada konu anlatımlarından farklı olarak profesyonel bir tasarımcıdan gelen tasarımları kullanacağız ve bunları da giydirmeyi göreceğiz. Uygulamamız bittiğinde aşağıdaki gibi görünecek:

Şimdi bu görsele ulaşabilmek için tasarımcımızın bize gönderdiği kesilmiş PNG dosyalarını ve tasarımı giydirirken bize kılavuzluk edecek genel görünüm dosyasını gözden geçirelim.

Gördüğünüz gibi tasarımcımız genel görünümle beraber görsel öğelerin yüksekliklerini, boyutlarını ve bunlarla beraber yazıtipleri ve büyüklüklerini detaylı bir şekilde anlatmış. Projenizde profesyonel bir tasarımcıyla çalışıyorsanız bu şekilde direktifler beklemeye hazır olun (göndermezse de göndermesini talep edin).

Bazı tasarımcılar PSD dosyalarının genel görünümünü PNG olarak kayıt edip özellikleri üzerine yazmayı tercih ederken, bazı tasarımcılar web üzerinde yardımcı sitelere genel tasarımı koyarak uygulama detaylarını anlatırlar. Metodlar farklılık gösterse de, geliştirici için önemli olan kodlarken uyması gereken kurallardır ve ebat, renk RGB değeri, kullanılan yazıtipi (font) gibi bilgileri tüm detaylarıyla bilmesi gerekir.

Şimdi uygulamaya başlamak için Android Studio içerisinden yeni bir proje oluşturalım ve bir adet Activity dosyası (MainActivity) ekleyelim. Öncelikle yapmamız gereken ilk ekrana karşılık gelen bu Activity dosyasının tasarımını (layout dosyası) güncellemek olacak. Tasarımcımızdan gelen verilere göre layout dosyasını (activity_main) aşağıdaki gibi oluşturuyoruz:

<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:background="@android:color/white"
    tools:context=".MainActivity" >
    <TextView
        android:id="@+id/topTitle"
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:background="@android:color/black"
        android:paddingLeft="20dp"
        android:paddingTop="5dp"
        android:text="@string/app_name"
        android:textColor="@android:color/white"
        android:textSize="30sp" />
    <ListView
        android:id="@+id/rssListview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@id/topTitle"
        android:background="@android:color/white" >
    </ListView>
</RelativeLayout>

Şimdi ise ListView içerisindeki satırları oluşturmamız gerekecek. Hatırlarsanız, Listview Özelleştirmesi konusunda bu amaçla ayrı bir sınıf oluşturulmuş ve bu sınıfın modellenen obje (bu durumda Rss sınıfı) ile ListView satırlarını oluşturması sağlanmıştı. Biz de burada RssListAdapter adında bir sınıf oluşturarak ListView'ın Rss sınıfında objeler taşıyan bir diziden dolduracağız.


package com.turkcell.readerapp.adapter;

import java.util.List;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.turkcell.readerapp.R;
import com.turkcell.readerapp.model.Rss;

public class RssListAdapter extends BaseAdapter {

   private List<Rss>      rssList;
   private LayoutInflater inflater;
   private Context        context;
   private ImageLoader    imageLoader;

   public RssListAdapter(Context context, List<Rss> rssList) {
      inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      this.rssList = rssList;
      this.context = context;
      this.imageLoader = ImageLoader.getInstance();
      this.imageLoader.init(ImageLoaderConfiguration.createDefault(context));
   }

   @Override
   public int getCount() {
      return rssList.size();
   }

   @Override
   public Object getItem(int position) {
      return rssList.get(position);
   }

   @Override
   public long getItemId(int position) {
      return position;
   }

   @Override
   public View getView(int position, View view, ViewGroup arg2) {
      Rss rss = rssList.get(position);
      View rowView = view;
      TextView txtTitle;
      TextView txtDate;
      if (rowView == null) {
         rowView = inflater.inflate(R.layout.row_main, null);
         txtTitle = (TextView) rowView.findViewById(R.id.rss_title);
         txtDate = (TextView) rowView.findViewById(R.id.rss_date);
         txtTitle.setTypeface(Typeface.createFromAsset(context.getAssets(), "MuseoSansRounded-700.otf"));
         txtDate.setTypeface(Typeface.createFromAsset(context.getAssets(), "HelveticaNeueMedium.ttf"));
      }
      txtDate = (TextView) rowView.findViewById(R.id.rss_date);
      txtTitle = (TextView) rowView.findViewById(R.id.rss_title);
      ImageView imageView = (ImageView) rowView.findViewById(R.id.rss_image);
      String imageUrl = rss.getImageUrl();
      imageView.setImageResource(R.drawable.ic_launcher);
      imageLoader.displayImage(imageUrl, imageView);
      txtTitle.setText(rss.getTitle());
      txtDate.setText(rss.getPostDate());
      return rowView;
   }
}

RssListAdapter içerisindeki private değişkenleri incelersek;

  • rssList: Rss objelerini içeren dizimiz.
  • inflater: LayoutInflater servisiyle satırları row_main.xml layout dosyasından oluşturmamızı sağlayan değişken.
  • context: Assets klasörü içerisinde yer alan yazıtiplerini kullanmamız için uygulama kaynaklarına erişmemizi sağlayacak Context değişkeni.
  • imageLoader: Satırlardaki görselleri eklememizi sağlayan ImageLoader değişkeni.

Constructor içerisinde bu değişkenleri tanımladıktan sonra metodları doldurmaya başlayabiliriz. getCount, getItem ve getItemId metotları rssList dizisi ile direkt ilişkili metotlardır. getView metodunu incelersek;



@Override
   public View getView(int position, View view, ViewGroup arg2) {

      Rss rss = rssList.get(position);
      View rowView = view;
      TextView txtTitle;
      TextView txtDate;
      if (rowView == null) {
         rowView = inflater.inflate(R.layout.row_main, null);
         txtTitle = (TextView) rowView.findViewById(R.id.rss_title);
         txtDate = (TextView) rowView.findViewById(R.id.rss_date);
         txtTitle.setTypeface(Typeface.createFromAsset(context.getAssets(), "MuseoSansRounded-700.otf"));
         txtDate.setTypeface(Typeface.createFromAsset(context.getAssets(), "HelveticaNeueMedium.ttf"));
      }
      txtDate = (TextView) rowView.findViewById(R.id.rss_date);
      txtTitle = (TextView) rowView.findViewById(R.id.rss_title);
      ImageView imageView = (ImageView) rowView.findViewById(R.id.rss_image);
      String imageUrl = rss.getImageUrl();
      imageView.setImageResource(R.drawable.ic_launcher);
      imageLoader.displayImage(imageUrl, imageView);
      txtTitle.setText(rss.getTitle());
      txtDate.setText(rss.getPostDate());
      return rowView;
   }

Burada her satır için ilgili RSS objesini çekerek satır içerisindeki tarih, metin ve resim değerlerini doldurmamız gerekiyor. Bunun için öncelikle rssList dizisinden o satıra denk gelen RSS objesini alıyoruz ve satırla ilgili görünümü (View) oluşturuyoruz. Görünümü oluştururken tasarımcının kullanılmasını istediği yazıtiplerini assets dizini içerisinde okumak için Typeface.createFromAsset metodundan faydalanırız. Tabii bu metodda adı geçen yazıtiplerinin assets dizinine önceden yüklenmiş olması gerekmektedir.

NOT: Bir klasöre dosya eklemek için dosyayı klasöre sürüklemeniz yeterlidir.

Satır görünümünü oluşturduktan sonra TextView değerlerini RSS objesi içerisinden ilgili değişkenlerle belirliyoruz. Burada ImageView haberle ilgili resmin görüntüleneceği kutu olacak. Kutu içerisinde yer alacak resimleri yine Rss objesinde ilgili değişkenle dolduruyoruz.

NOT: Resim yükleme ve görüntülenme işleminin kullanıcı deneyimine zarar vermemesi için Asenkron yapılması (UI Thread'in - ana akış - bozulmaması) gerekmektedir. Biz bu amaçla harici bir kütüphane olan Android Universal Image Loader'dan faydalandık. libs klasörü altında jar dosyasını bulabileceğiniz gibi https://github.com/nostra13/Android-Universal-Image-Loader adresinden projenin tüm kodlarını indirebilirsiniz.

Satırı oluşturan layout dosyası aşağıdaki gibidir:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:background="@android:color/white"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/rss_date"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:lines="1"
        android:paddingLeft="36dp"
        android:singleLine="true"
        android:textSize="16sp" />
    <TextView
        android:id="@+id/rss_title"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:lines="2"
        android:paddingLeft="30dp"
        android:paddingRight="12dp"
        android:singleLine="false"
        android:textColor="@android:color/black"
        android:textSize="22sp" />
    <ImageView
        android:id="@+id/rss_image"
        android:layout_width="340dp"
        android:layout_height="340dp"
        android:layout_gravity="center_horizontal"
        android:layout_margin="4dp"
        android:paddingLeft="36dp"
        android:contentDescription="@string/app_name"
         />
</LinearLayout>

MainActivity içerisine geri dönersek onCreate metodu içerisinde gerekli değişiklikleri yaparak RSS okuma işlemini gerçekleştirebiliriz;

 



package com.turkcell.readerapp.activity;

import java.util.List;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import com.turkcell.readerapp.R;
import com.turkcell.readerapp.adapter.RssListAdapter;
import com.turkcell.readerapp.asynctask.ParseFeedAsyncTask;
import com.turkcell.readerapp.asynctask.callback.ParseFeedCallback;
import com.turkcell.readerapp.model.Rss;

public class MainActivity extends Activity implements ParseFeedCallback {
   private ListView listView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      listView = (ListView) findViewById(R.id.rssListview);
      listView.setOnItemClickListener(new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
            Rss rss = (Rss) listView.getAdapter().getItem(position);
            Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(rss.getOriginalPostUrl().trim()));
            startActivity(browserIntent);
         }
      });

      new ParseFeedAsyncTask(this).execute((Void) null);
   }
   @Override
   public void finishedLoadingFeeds(List<Rss> feeds) {
      listView.setAdapter(new RssListAdapter(getApplicationContext(), feeds));
   }

}

 

Daha önceki bölümlerde olduğu gibi öncelikle layout dosyası içerisinden ListView öğesini çağırıp az önce oluşturduğumuz Adapter yardımıyla dizi içerisinde yer alan öğeleri listeliyoruz. Kullanıcı bir satıra tıkladığında ise onItemClickListener metodu devreye giriyor ve haberin orijinal sitesi bir web tarayıcı penceresinde açılıyor.

RSS'lerin uzaktaki bir kaynaktan okunması tıpkı resim yükleme işleminde olduğu gibi asenkron gerçekleştirilmek zorundadır. Aksi takdirde uygulama arayüzü (UI Thread) yükleme işlemi bitene kadar duracak ve kullanıcı uygulamamızın bozulduğunu varsayacaktır. Yükleme işlemini arka planda gerçekleştirmek için ParseFeedAsyncTask adında bir AsyncTask sınıfı oluşturuyoruz;

 




package com.turkcell.readerapp.asynctask;

import java.util.List;
import android.os.AsyncTask;
import com.turkcell.readerapp.asynctask.callback.ParseFeedCallback;
import com.turkcell.readerapp.helper.Downloader;
import com.turkcell.readerapp.helper.RssParser;
import com.turkcell.readerapp.model.Rss;

public class ParseFeedAsyncTask extends AsyncTask<Void, Void, List<Rss>> {
   private static final String feedUrl = "http://www.iphoneturkey.com/feed/";
   private ParseFeedCallback   callback;
   public ParseFeedAsyncTask(ParseFeedCallback callback) {
      this.callback = callback;
   }
   @Override
   protected List<Rss> doInBackground(Void... params) {
      String xmlContent = Downloader.getContent(feedUrl);
      return RssParser.parseFeed(xmlContent);
   }
   @Override
   protected void onPostExecute(List<Rss> result) {
      super.onPostExecute(result);
      callback.finishedLoadingFeeds(result);
   }
}

 

package com.turkcell.readerapp.asynctask.callback;

import java.util.List;
import com.turkcell.readerapp.model.Rss;

public interface ParseFeedCallback {  
   public void finishedLoadingFeeds(List<Rss> feeds);  
}

 

RSS kaynağından gelen haberleri modellemek için Rss adında bir sınıf kullanırız;


package com.turkcell.readerapp.model;

public class Rss {

   private String title;
   private String content;
   private String postDate;
   private String imageUrl;
   private String originalPostUrl;

   public String getTitle() {
      return title;
   }

   public void setTitle(String title) {
      this.title = title;
   }

   public String getContent() {
      return content;
   }

   public void setContent(String content) {
      this.content = content;
   }

   public String getPostDate() {
      return postDate;
   }

   public void setPostDate(String postDate) {
      this.postDate = postDate;
   }

   public String getImageUrl() {
      return imageUrl;
   }

   public void setImageUrl(String imageUrl) {
      this.imageUrl = imageUrl;
   }

   public String getOriginalPostUrl() {
      return originalPostUrl;
   }

   public void setOriginalPostUrl(String originalPostUrl) {
      this.originalPostUrl = originalPostUrl;
   }
}

RSS kaynağını yukarıdaki sınıfa çevirmek içinse Parser adı verilen bir sınıf hazırlamak zorundayız. Java'da bu amaçla kullanılabilecek çeşitli kütüphaneler mevcuttur. Biz ise Android içerisinde de yer alan XmlPullParser adlı kütüphaneden faydalanacağız:



package com.turkcell.readerapp.helper;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import android.util.Log;
import com.turkcell.readerapp.model.Rss;

public class RssParser {

   public static List<Rss> parseFeed(String feedContent) {
      List<Rss> results = new ArrayList<Rss>();
      Rss rss = new Rss();
      try {
         XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
         factory.setNamespaceAware(false);
         XmlPullParser xpp = factory.newPullParser();

         xpp.setInput(new StringReader(feedContent));

         int eventType = xpp.getEventType();
         while (eventType != XmlPullParser.END_DOCUMENT) {

            if (eventType == XmlPullParser.START_DOCUMENT) {

            }
            else if (eventType == XmlPullParser.START_TAG) {
               String tagName = xpp.getName();
               if ("item".equals(tagName)) {
                  rss = new Rss();
               }
               else if ("description".equals(tagName)) {
                  String desc = xpp.nextText();
                  rss.setContent(desc);

                  Pattern p = Pattern.compile(".*<img[^>]*src=\"([^\"]*)", Pattern.CASE_INSENSITIVE);
                  Matcher m = p.matcher(desc);
                  String srcUrl = null;
                  while (m.find()) {
                     srcUrl = m.group(1);
                     rss.setImageUrl(srcUrl.toString().trim());
                  }
               }
               else if ("dc:date".equals(tagName)) {
                  rss.setPostDate(xpp.nextText());
               }
               else if ("pubDate".equals(tagName)) {
                  rss.setPostDate(xpp.nextText());
               }
               else if ("title".equals(tagName)) {
                  rss.setTitle(xpp.nextText());
               }
               else if ("link".equals(tagName)) {
                  rss.setOriginalPostUrl(xpp.nextText());
               }
            }
            else if (eventType == XmlPullParser.CDSECT) {
            }
            else if (eventType == XmlPullParser.END_TAG) {
               String tagName = xpp.getName();
               if ("item".equals(tagName)) {
                  results.add(rss);
               }
            }
            else if (eventType == XmlPullParser.TEXT) {
            }
            eventType = xpp.nextToken();

         }
      }
      catch (Exception e) {
         e.printStackTrace();
         Log.e("Main", "Error : " + e.getMessage());
      }
      return results;
   }
}

RssParser sınıfını içerisinde tanımladığımız parseFeed metodu verilen RSS kaynağı içerisinde yer alan her haberi bir dizi şeklinde bize döndürecektir. Bunun için öncelikle bir XmlPullParser oluşturup StringReader yardımıyla işlemi başlatıyoruz. eventType değişkeni parse işlemi sırasında karşılaşılan değişik durumları bize bildirecektir. Buradaki değerler şu şekildedir:

  • -END_DOCUMENT: Belgenin bittiğini gösterir. Bu noktada dizimizde bütün haberler yer alacaktır.
  • -START_DOCUMENT: Belgenin en başını işaret eder.
  • -START_TAG: Yeni bir etiketin başlangıcını haber verir. Eğer etiket item değerini taşıyorsa yeni bir haber gelecek. Biz de bu haberi kayıt için yeni bir RSS objesi oluştururuz. Eğer işimize yarayan bir etiket varsa (description, pubDate, title, link) etiketin değerini nextText metodu ile alıp RSS objesi içerisinde ilgili yere tanımlarız.
  • -END_TAG: Etiketin bitişini belirtir. </item> etiketi bir haberi sonlandırdığımız anlamına gelir ve Rss objesini resultsız.

NOT: Sadece haberle ilgili resim bağlantısını almak için description etiketi içerisinde ufak bir regex işlemi yapmamız gereklidir. İlgilendiğimiz resim bağlantısı <image src=”” .../> etiketi içerisindeki src eklentisinde olduğundan bağlantıyı alacak bir regex kullanırız.

Uygulamamıza ait AndroidManifest dosyası ise aşağıdaki gibidir. Burada İnternet iznini vermeyi unutmamalıyız.



 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.turkcell.readerapp"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
        <activity android:name="com.turkcell.readerapp.activity.MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

Downloader sınıfını da projenize eklemeniz gerekmektedir.

package com.turkcell.readerapp.helper;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

public class Downloader {

   public static String getContent(String url) {
      StringBuilder sb = new StringBuilder();
      try {
         URL remoteUrl = new URL(url);
         URLConnection yc = remoteUrl.openConnection();
         BufferedReader in = new BufferedReader(new InputStreamReader(yc.getInputStream()));
         String inputLine;
         while ((inputLine = in.readLine()) != null)
            sb.append(inputLine);
         in.close();
      }
      catch (Exception e) {
      }
      return sb.toString();
   }

}