RSS-Feed zu Json und Weiterverarbeitung in Apps

von Ludy geschrieben am und aktualisiert am
Kategorien RSS-Feed
Schlagwörter , , ,

Ich zeige euch heute wie man aus einem RSS-Feed einen Json-String erstellt und weiterverarbeitet.

Disclaimer

Alles geschieht auf eigene Verantwortung, weder die Softwareentwickler noch ich übernehmen Verantwortung für Schäden an Hard- und Software.

Was wird gebraucht?
– einen Server der PHP und DOMDocument beherrscht.
– einen Texteditor, ich nutze Notepad++ UTF-8 ohne BOM
Android Studio

Nun geht es los:

Als erstes erstellen wir uns eine PHP-Datei (rss.php), mit den gewohnten PHP-Tags.

<?php 

?>

In dem PHP-Tag schreiben wir die Header-Information:

  • Type Json
  • encoding UTF-8.
    header("Content-type: application/json; charset=utf-8");
    

Nun erstellen wir die Variable ($rss_url) für die Url des Feeds und erstellen ein Objekt ($feed) der Klasse DOMDocument. Wenn das geschafft ist, laden wir die Informationen von der Seite des RSS-Feeds in $feed.

$rss_url = 'https://www.androidpit.de/feed/main.xml';

$feed = new DOMDocument();
$feed->load($rss_url);

Mit vier Codezeilen haben wir die halbe Miete, dann holen wir uns nun noch die zweite Hälfte.

Was wir jetzt brauchen, finden wir im Feed selber (er sollte als Quellcode betrachtet werden).

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <title>...</title>
    <link>https://www.androidpit.de/</link>
    <description>...</description>
    <language>de</language>
    <pubDate>Tue, 05 Jul 2016 09:52:04 GMT</pubDate>
    <dc:creator>AndroidPIT</dc:creator>
    <dc:date>2016-07-05T09:52:04Z</dc:date>
    <dc:language>de</dc:language>
    <item>
      <title>...</title>
      <link>https://...</link>
      <description>...</description>
      <pubDate>...</pubDate>
      <guid isPermaLink="false">...</guid>
      <dc:creator>...</dc:creator>
      <dc:date>...</dc:date>
    </item>
  </channel>
</rss>

Schematisch sieht das Ganze so aus, jetzt wird das wichtigste herausgesucht und das bleibt dann über.

  <channel>
    <title>...</title>
    <link>https://www.androidpit.de/</link>
    <description>...</description>
    <language>de</language>
    <pubDate>...</pubDate>
    <dc:creator>AndroidPIT</dc:creator>
    <item>
      <title>...</title>
      <link>https://...</link>
      <description>...</description>
      <pubDate>...</pubDate>
      <dc:creator>...</dc:creator>
    </item>
  </channel>

Okay, dann wissen wir das channel-Tag unser Eltern-Tag ist, von dem alles ausgeht.
Zunächst erstellen wir uns ein Array() ($json).

Channel besitzt mehrere „einfache“ (title, link, description, language, pubDate, dc:creator) Kinder und ein „kompliziertes“ (item) Kind. Da nachher ein Json-String entstehen soll, geben wir $json den title, link description, language, pubDate und dc:creator mit.

An dc:creator seht ihr eine Besonderheit, es gibt keinen „normalen“ TagName(creator) sondern einen TagName mit Namespace (dc).

Die Namespace-URI findet ihr im Tag rss unter dem Attrebut xmls:{Namespace}=URI
sie lautet http://purl.org/dc/elements/1.1/, diese muss immer dort angewendet werden, wo kein normaler Tagname definiert ist, nun gut, noch eine Variable geschrieben.

<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
$slashNS =             "http://purl.org/dc/elements/1.1/";

$parentElement =       $feed->getElementsByTagName('channel')->item(0);

$json = array();

$json['title'] =       $parentElement->getElementsByTagName('title')->item(0)->firstChild->nodeValue;
$json['link'] =        $parentElement->getElementsByTagName('link')->item(0)->firstChild->nodeValue;
$json['description'] = $parentElement->getElementsByTagName('description')->item(0)->firstChild->nodeValue;
$json['language'] =    $parentElement->getElementsByTagName('language')->item(0)->firstChild->nodeValue;
$json['pubDate'] =     $parentElement->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;
$json['creator'] =     $parentElement->getElementsByTagNameNS($slashNS, 'creator')->item(0)->firstChild->nodeValue;

Der Header-Teil des RSS-Feed ist nun schon einmal fertig und könnte theoretisch Json encoded werden, aber ein Feed soll mehr sein, er soll die Infos der Seite widerspiegeln.
Nun werden wir die item Kinder-Tags untersuchen – enthalten ist ‚title‘, ‚link‘, ‚description‘, ‚pubDate‘ und ‚dc:creator‘.

Da so ein Feed bzw. Artikel mehrere Informationen liefert, werden die item auch mehrfach vorkommen.
Daher definieren wir eine Variabel items ($items) und dieser übergeben wir alle ‚item‘-Tags.
Dem Json-Array geben wir noch einen Array (item) mit.

$items = $parentElement->getElementsByTagName('item');
$json['item'] = array();

Um $json[‚item‘] zu füllen brauchen wir eine ‚foreach‘-Schleife, sie wird solange ausgeführt bis kein ‚item‘-Tag mehr vorhanden ist. Jetzt gehen wir wie beim Headerteil vor – Variablen erstellen von ‚title‘, ‚link‘, ‚description‘, ‚pubDate‘ und ‚dc:creator‘.

$json[‚item‘] wird in der Schleife befüllt, aber dass nicht immer wieder unserer Wert im Array überschrieben wird, ergänzen wir diesen mit einem Array() in der Schleife.

Aus ‚title‘, ‚link‘, ‚description‘, ‚pubDate‘ und ‚dc:creator‘ erstellen wir innerhalb der Schleife einen assoziativen Array()

$json['item'][] = array("title"=>$title, "link"=>$link, "description"=>$description, "pubDate"=>$pubDate, "creator"=>$creator);
foreach ($items as $item) {
    $title =          $item->getElementsByTagName('title')->item(0)->firstChild->nodeValue;
    $link =           $item->getElementsByTagName('link')->item(0)->firstChild->nodeValue;
    $description =    $item->getElementsByTagName('description')->item(0)->firstChild->nodeValue;
    $pubDate =        $item->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;
    $creator =        $item->getElementsByTagNameNS($slashNS, 'creator')->item(0)->firstChild->nodeValue;
    $json['item'][] = array("title"=>$title,"link"=>$link,"description"=>$description,"pubDate"=>$pubDate,"creator"=>$creator);
}

Und als letzten Schritt geben wir unser Resultat aus.

echo json_encode($json);

Das Resultat sollte nun so aussehen.

<?php 
    header("Content-type: application/json; charset=utf-8");

    $rss_url = 'https://www.androidpit.de/feed/main.xml';

    $feed = new DOMDocument();
    $feed->load($rss_url);

    $slashNS = "http://purl.org/dc/elements/1.1/";
    $parentElement = $feed->getElementsByTagName('channel')->item(0);

    $json = array();
    $json['title'] =       $parentElement->getElementsByTagName('title')->item(0)->firstChild->nodeValue;
    $json['link'] =        $parentElement->getElementsByTagName('link')->item(0)->firstChild->nodeValue;
    $json['description'] = $parentElement->getElementsByTagName('description')->item(0)->firstChild->nodeValue;
    $json['language'] =    $parentElement->getElementsByTagName('language')->item(0)->firstChild->nodeValue;
    $json['pubDate'] =     $parentElement->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;
    $json['creator'] =     $parentElement->getElementsByTagNameNS($slashNS, 'creator')->item(0)->firstChild->nodeValue;

    $items = $parentElement->getElementsByTagName('item');

    $json['item'] = array();
    foreach ($items as $item) {
        $title =          $item->getElementsByTagName('title')->item(0)->firstChild->nodeValue;
        $link =           $item->getElementsByTagName('link')->item(0)->firstChild->nodeValue;
        $description =    $item->getElementsByTagName('description')->item(0)->firstChild->nodeValue;
        $pubDate =        $item->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;
        $creator =        $item->getElementsByTagNameNS($slashNS, 'creator')->item(0)->firstChild->nodeValue;
        $json['item'][] = array("title"=>$title,"link"=>$link,"description"=>$description,"pubDate"=>$pubDate,"creator"=>$creator);
    }
    echo json_encode($json);
?>

Die Verarbeitung eines Json-String ist einfacher als ein XML-String, die Fehlerquote beim Parsen durch GSON geht gegen Null.

Erstellt ein neues Projekt in Android Studio, wie das geht wird in diesem Thread nicht weiter erläutert.

Benötigte Library

  • compile ‚com.android.support:appcompat-v7:25.0.1‘
  • compile ‚com.android.support:cardview-v7:25.0.1‘
  • compile ‚com.android.support:recyclerview-v7:25.0.1‘
  • compile ‚com.google.code.gson:gson:2.8.0‘

Als erstes brauchen wir ein Layout, was RecyclerView beinhaltet. Das RecyclerView wird mit einem LinearLayout umschlossen.

activity_feed.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    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"
    android:background="#b65757">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>
</LinearLayout>

FeedActivity wird unsere Start-Activity sein. Initialisieren werden wir RecyclerView.

FeedActivity.java

public class FeedActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feed);
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    }

}

Okay, nun haben wir schon einmal die Bases.
Um das Json-Objekt in die Activity zu bekommen, brauchen wir eine Internetverbindung (Permission in der AndroidManifest.xml deklarieren).
Was brauchen wir noch, genau eine Schnittstelle! Zu beachten ist das solche Schnittstellen nicht in der Activity-Laufzeit ausgeführt werden dürfen, sie müssen immer in einem Thread() ausgeführt werden. In unserem Fall verwenden wir einen AsyncTask. Dazu erstellen wir uns eine neue Klasse LoadRss.java und erweitern diese mit

AsyncTask<String, String, String>

LoadRss.java

public class LoadRss extends AsyncTask<String, String, String> {

    @Override
    protected String doInBackground(String... params) {
        return null;
    }

    @Override
    protected void onPostExecute(String s) {

    }
}

Die FeedActivity#onCreate können wir nun mit new LoadRss().execute(); ausstatten.

FeedActivity.java

public class FeedActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // [...]
        // gekürtzt
        new LoadRss().execute();
    }

}

Bevor wir die LoadRss.java mit allem was dazu gehört ausstatten, erstellen wir ein Interface und eine Klasse, die dieses Implementiert.

RssResultInterface.java

public interface RssResultInterface {
    void onResult(String result);

    void onResponseCode(int responseCode);
}

Das onResult(String) wird uns später das Ergebnis von der #onPostExecute liefern und onResponseCode(int) den ResponseCode aus #doInBackground. Um den Code in der FeedActiviy.java übersichtlich zu halten, implementieren wir RssResultInterface.java in RssResult.java. Durch diese Implementierung müssen wir nicht alle Methoden in der FeedActivity.java implementieren, wenn RssResult aufgerufen wird. Was ich meine wird euch später klar.

RssResult.java

public class RssResult implements RssResultInterface {
    @Override
    public void onResult(String result) {

    }

    @Override
    public void onResponseCode(int responseCode) {

    }
}

Um das Interface nutzen zu können, initialisieren wir RssResult.java und übergeben wir dieses der LoadRss.java.

Nun seht ihr, dass das #onResponseCode nicht mit implementiert werden muss

private RssResult rssInterface = new RssResult() {
    @Override
    public void onResult(String result) {

    }
};

FeedActivity#onCreate

new LoadRss(rssInterface).execute();

So würde es mit dem „normalen“ Interface (RssResultInterface.java) aussehen.

private RssResultInterface rssResultInterface = new RssResultInterface() {
    @Override
    public void onResult(String result) {

    }

    @Override
    public void onResponseCode(int responseCode) {

    }
};

Die LoadRss.java benötigt nun einen Konstruktor, der als Parameter RssResult beinhaltet.

private RssResult rssResultInterface;

public LoadRss(RssResult rssInterface) {
    this.rssResultInterface = rssInterface;
}

Das Interface und die fehlenden Komponenten in LoadRss.java werden wir nun hinzufügen. Wir verwenden HttpURLConnection und erstellen eine Hilfsklasse StreamToString.java.

LoadRss.java

public class LoadRss extends AsyncTask<String, String, String> {

    private RssResult rssResultInterface;
    private static final String TAG = LoadRss.class.getSimpleName();

    public LoadRss(RssResult rssInterface) {
        this.rssResultInterface = rssInterface;
    }

    @Override
    protected String doInBackground(String... params) {
        try {
            URL url = new URL(params[0]);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.connect();
            rssResultInterface.onResponseCode(connection.getResponseCode());
            return StreamToString.streamToString(connection.getInputStream());
        } catch (IOException e) {
            Log.e(TAG, e.getMessage());
        }
        return null;
    }

    @Override
    protected void onPostExecute(String s) {
        rssResultInterface.onResult(s);
    }
}

Ihr seht richtig, mehr Text gibt es in der Klasse nicht.

URL url = new URL(params[0]);
// params[0] ist die Url, welche noch in der FeedActivity.java übergeben werden muss.

rssResultInterface.onResponseCode(connection.getResponseCode());
// die Methode vom Interface bekommt an dieser Stelle den ResponseCode übergeben

rssResultInterface.onResult(s);
// die Methode vom Interface bekommt an dieser Stelle den JsonObjekt übergeben als String

Aber einen Fehler gibt es noch

StreamToString.streamToString(connection.getInputStream());

Hier benötigen wir die Hilfsklasse StreamToString.java diese nun erstellt werden muss

StreamToString.java

public class StreamToString {
    public static String streamToString(InputStream is) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));
        String line;
        while ((line = rd.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    }
}

FeedActivity

private static final String RSS_URL = "http://192.168.0.101/rss.php"; // Hier euren Server eintragen

FeedActivity#onCreate

new LoadRss(rssInterface).execute(RSS_URL);

Im dritten Teil werden wir uns in der FeedActivity.java mit dem Interface weiter beschäftigen.

Als erstes sollte wir das ganze debuggen.

private static final String TAG = FeedActivity.class.getSimpleName();

private RssResult rssInterface = new RssResult() {
    @Override
    public void onResult(String result) {
        if (result != null) {
            Log.d(TAG, result);
        }
    }
};

Als Ergebnis sollte unser JsonObjekt als String in der Konsole auftauchen.

{title:“…“,“link“:“…“,……….}

Im nächsten Schritt kümmern wir uns um die item-Elemente, der Schlüssel „item“ ist ein Array(), dessen Kinder sind Objekte und deren Kinder sind Strings.

Okay dann erstellen wir uns eine neue Klasse Item.java. Die Schlüssel in den einzelnen item-Objekten lauten:

title, link, description, pubDate und creator

dazu erstellen wir noch die einzelnen Getters.

Item.java

public class Item {

    private String title;
    private String link;
    private String description;
    private String pubDate;
    private String creator;

    public String getCreator() {
        return creator;
    }

    public String getDescription() {
        return description;
    }

    public String getLink() {
        return link;
    }

    public String getPubDate() {
        return pubDate;
    }

    public String getTitle() {
        return title;
    }
}

Erstellt eine Klasse (Parent) und implementiert Serializable um etwas flexibler zu sein.

Parent.java

public class Parent implements Serializable {

    @SerializedName("title")
    private String parentTitle;
    @SerializedName("link")
    private String parentLink;
    @SerializedName("description")
    private String parentDescription;
    @SerializedName("language")
    private String parentLanguage;
    @SerializedName("pubDate")
    private String parentPubDate;
    @SerializedName("creator")
    private String parentCreator;
    @SerializedName("item")
    private Item[] parentItems;

    public String getParentCreator() {
        return (parentCreator);
    }

    public String getParentDescription() {
        return parentDescription;
    }

    public Item[] getParentItems() {
        return parentItems;
    }

    public String getParentLanguage() {
        return parentLanguage;
    }

    public String getParentLink() {
        return parentLink;
    }

    public String getParentPubDate() {
        return parentPubDate;
    }

    public String getParentTitle() {
        return parentTitle;
    }
}

SerializedName ist der „echte“ Schlüssel des Json-Objekt/-Array, darunter ist unser definierter String mit unserem Namen.

Wenn das Resultat stimmt, können wir weiter machen in der FeedActivity.java, und schmücken private RssResult rssInterface = new RssResult() {onResult} mit ein paar Zeilen Code. Wir werden nun den String in Json konvertieren und unseren Variablen deklarieren und die Daten übergeben.

Gson parentGson = new Gson();

Parent parent = parentGson.fromJson(result, Parent.class);
String parentTitle = parent.getParentTitle();
String parentLink = parent.getParentLink();
String parentDescription = parent.getParentDescription();
String parentLanguage = parent.getParentLanguage();
String parentPubDate = parent.getParentPubDate();
String parentCreator = parent.getParentCreator();

Eine ArrayList die unsere items beinhaltet fehlt uns noch, na dann erstellen und diese für den CustomAdapter übergabefähig machen.

Item[] items = parent.getParentItems();
final ArrayList<Item> list = new ArrayList<>();
Collections.addAll(list, items);

Wir brauchen jetzt einen CustomAdapter für unseren RecyclerView, dazu benötigen wir ein Layout für das CardView (card_view_row.xml).

card_view_row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp"
    android:paddingRight="8dp">

    <android.support.v7.widget.CardView
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:card_view="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardUseCompatPadding="true"
        card_view:cardCornerRadius="8dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="8dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/textViewCreator"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="textViewCreator"
                android:textAppearance="?android:attr/textAppearanceSmall"/>

            <TextView
                android:id="@+id/textViewTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="textViewTitle"
                android:textAppearance="?android:attr/textAppearanceMedium"/>

            <TextView
                android:id="@+id/textViewDescription"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:maxLines="4"
                android:text="textViewDescription"/>

            <TextView
                android:id="@+id/textViewPubDate"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="textViewPubDate"
                android:textAppearance="?android:attr/textAppearanceSmall"/>
        </LinearLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

Das Layout ist zwar nicht wirklich schön soll aber für uns erst einmal reichen. Jetzt kommen wir zum Adapter (RssFeedAdapter.java) dieser wird erweitert mit

RecyclerView.Adapter<RssFeedAdapter.ViewHolder>

RssFeedAdapter.ViewHolder ist eine innen liegende Klasse, die erweitert wird mit

RecyclerView.ViewHolder

In der onCreateViewHolder wird das Layout card_view_row.xml inflated und dem RssFeedAdapter.ViewHolder bei der Erstellung mitgegeben.

Die onBindViewHolder ist die Methode, in der aus der Arrayliste von items, die einzelnen Item’s ausgelesen werden und je einer CardView zugewiesen wird. Da es eine Arraylist ist, können wir bequem über die Position des Views, die Position des Item bzw. des Inhalt abfragen.

Item items = item.get(position);

Als nächsten Schritt, werden wir die einzelnen View füllen und dem Adapter mitteilen wie viele ArrayObjecte vorhanden sind.

holder.title.setText(items.getTitle());
holder.creator.setText(items.getCreator());
holder.description.setText(items.getDescription());
holder.pubDate.setText(items.getPubDate());


@Override
public int getItemCount() {
    return itemList.size();
}

RssFeedAdapter.java

public class RssFeedAdapter extends RecyclerView.Adapter<RssFeedAdapter.ViewHolder> {

    private ArrayList<Item> itemList;

    public RssFeedAdapter(ArrayList<Item> itemList) {
        this.itemList = itemList;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.card_view_row, null, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Item items = itemList.get(position);
        holder.title.setText(items.getTitle());
        holder.creator.setText(items.getCreator());
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            holder.description.setText(Html.fromHtml(items.getDescription(), Html.FROM_HTML_MODE_LEGACY));
        } else {
            holder.description.setText(Html.fromHtml(items.getDescription()));
        }
        holder.pubDate.setText(items.getPubDate());
    }

    @Override
    public int getItemCount() {
        return itemList.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {

        private TextView title;
        private TextView creator;
        private TextView description;
        private TextView pubDate;

        public ViewHolder(View itemView) {
            super(itemView);
            this.title = (TextView) itemView.findViewById(R.id.textViewTitle);
            this.creator = (TextView) itemView.findViewById(R.id.textViewCreator);
            this.description = (TextView) itemView.findViewById(R.id.textViewDescription);
            this.pubDate = (TextView) itemView.findViewById(R.id.textViewPubDate);
        }
    }
}

Das Ende naht, wir erweitern den Teil:

Item[] items = parent.getParentItems();
final ArrayList<Item> list = new ArrayList<>();
Collections.addAll(list, items);

Die Adapter Variable dürfen wir nicht vergessen!

private RssFeedAdapter rssFeedAdapter;

rssFeedAdapter noch initialisieren mit new RssFeedAdapter und die Liste nicht vergessen – Adapter dem RecyclerView zuweisen.

rssFeedAdapter = new RssFeedAdapter(list);
mRecyclerView.setAdapter(rssFeedAdapter);
Item[] items = parent.getParentItems();
final ArrayList<Item> list = new ArrayList<>();
Collections.addAll(list, items);
rssFeedAdapter = new RssFeedAdapter(list);
mRecyclerView.setAdapter(rssFeedAdapter);

Das Ganze nun debuggen und schau ob es läuft.

Aus einer ListView sind wir gewohnt, den Eintrag an zu klicken und eine Aktion wird ausgeführt, beim RecyclerView gibt es nicht den OnItemClickListener, hm unschön.
Aber die Jungs von Little Robots haben eine Hilfsklasse vorgestellt und die werden wir uns zu eigen machen.

ItemClickSupport.java

/**
 * http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/
 */
public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };
    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };
    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
            = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override
        public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    public interface OnItemClickListener {

        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    public interface OnItemLongClickListener {

        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}

In values brauchen wir noch die ids.xml.

ids.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="item_click_support" type="id"/>
</resources>

Okay, nun ab in die FeedActivity.java und den Listener ans RecyclerView binden.

private RssResult rssInterface = new RssResult() {
    @Override
    public void onResult(String result) {
        if (result != null) {
           //
           // gekürzt
           //
            mRecyclerView.setAdapter(rssFeedAdapter);

            ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
                @Override
                public void onItemClicked(RecyclerView recyclerView, int position, View v) {

                }
            });

        }
    }
};

Wir werden den Link, des einzelnen Artikels, per Intent in das Android System „feuern“. Dazu nutzen wir die ArrayList(), die einzelnen Elemente können wir durch die Nennung der Position auslesen.

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(list.get(position).getLink()));
startActivity(intent);

Das vollständige Projekt findet ihr hier.

Erstellt euch auf der Firebase Console, entweder ein „neues Projekt“ oder importiert ein „Google-Projekt“.

User uploaded photo

Wenn das geschafft ist – was, denke ich, selbsterklärend ist – gehen wir, wenn nicht automatisch, in unser Projekt. Dazu klickt ihr einfach auf User uploaded photo

In nächstem Schritt wählt ihr „Firebase meiner Android-App hinzufügen“

User uploaded photo

Wodurch ihr nun eure App-Daten eintragen könnt, wichtig ist das ihr exakt den Paketnamen von eurem Android-Projekt eintragt. Der SHA1-Wert ist in unserem Fall nicht wichtig. Mit dem Klick auf Hinzufügen wird eine Datei (google-services.json) erstellt und in eurem Browser heruntergeladen.

User uploaded photo

Die Datei (google-services.json) ist sehr wichtig, für den weiteren Verlauf, für das Android-Projekt. In dem Fenster wird euch beschrieben wo ihr diese Datei (google-services.json) einfügen müsst und was in den build.gradle Dateien ergänzen werden muss, bitte mach das wie beschrieben.

Eure App wurde nun hinzugefügt und ist als Modul dargestellt. In diesem Modul ist, in der rechten oberen Ecke, ein Drei-Punkte-Menu, darauf klicken und Verwalten auswählen.

User uploaded photo

Oben in der Navigationsleiste findet ihr den Punkt Allgemein von da benötigen wir die Project ID und Cloud Messaging unter diesem ist der Serverschlüssel aufgelistet.
Diesen Schlüssel nicht öffentlich stellen

Bevor mit der Android-App weiter gemacht wird, kümmern wir uns um die serverseitigen Funktionen. Zunächst erstellen wir eine Utility-Datei (utils.php) um unseren Code übersichtlich zu halten. Sie wird im laufe immer mal wieder erweitert.

Euch wird vielleicht aufgefallen sein, dass die Datumanzeige nicht der Deutschenformatierung entspricht.

IST: Wed, 13 Jul 2016 18:49:02 GMT
SOLL: 13.07.2016 18:49:02

utils.php

<?php

    function createDate($oldDate) {
        return DateTime::createFromFormat('D, d M Y H:i:s', substr($oldDate, 0, strlen($oldDate)-4))->format('d.m.Y H:i:s');
    }
?>

in der rss.php muss folgendes geändert werden:
aus

$json['pubDate'] =         $parentElement->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;

wird

$json['pubDate'] =         createDate($parentElement->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue);

aus

$pubDate = $item->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue;

wird

$pubDate = createDate($item->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue);

Über dem Eintrag header(„Content-type: application/json; charset=utf-8“); fügen wir nun die require_once(‚./utils.php‘); Zeile ein, dadurch ist die Funktion in der rss.php aufrufbar.

Es folgt jetzt der Grundaufbau für die Firebase Cloud Messaging in PHP. Der einfach halber nennen wir sie index.php. Für das Script benötigen wir den Serverschlüssel und die Firebase Cloud Messaging Url.

index.php

<?php 
    define(SERVER_KEY, "");                 // euer Serverschlüssel
    define(TOPIC_ADRESS, "/topics/info");   // identifiziert unsere Nachricht als unsere und sendet an alle Geräte die registriert sind
    define(FIREBASE_SERVER_FCM, "https://fcm.googleapis.com/fcm/send");
    header("Content-type: application/json; charset=utf-8");

    $result_rss    = requireToVar('./rss.php');        // hier wird unsere rss.php eingelesen um den generierten JsonString zu verwenden
    $items         = json_decode($result_rss, true)['item'];  // filtert nur die Item's

    foreach($items as $item) {
        if (substr($item['pubDate'], 0, strlen($item['pubDate'])-9) == date('d.m.Y')) {       
            echo "heute";
        } else {
            echo "anderer Tag";
        }
    }

    //
    // einlesen von Server eigenen Dateien
    //
    function requireToVar($file){
        ob_start();      // Ausgabepufferung aktivieren, verhindert das unsere rss.php gleich ausführt wird
        require($file);
        return ob_get_clean();
    }
?>

Mit er foreach-Schleife durchlaufen wir alle Item’s Objekte, zunächst benötigen wir erstmal nur das Datum, welches in TEIL 7 in das Deutschformat geändert wurde.
In der if-Abfrage schauen wir, ob das Datum im Feed mit dem deutschen Server-Datum übereinstimmt. Warum nutzen wir aber substr? Ganz einfach unser pubDate hat die Uhrzeit mit im String, die muss da raus.

Wenn ihr das Script nun ausführt sollte erst heute aufgelistet werden und danach anderer Tag.

Alles O.K.? Gut dann zur utils.php und eine Funktionen hinzufügen, sie sendet nachher unsere Nachricht -> Sending topic messages from the server

utils.php

function send($title, $creator, $link) {
    $post_content = array('to' => TOPIC_ADRESS, 'data' => array('title' => $title, "creator" => $creator, "link" => $link));
    $httpheader = array('Content-Type:application/json', 'Authorization:key='.SERVER_KEY);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, FIREBASE_SERVER_FCM);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_content));
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}

In der if-Abfrage von der index.php ergänzen wir eine Zeile.

index.php

echo send($item['title'], $item['creator'], $item['link']);

Den echo-Befehl habe ich nur zur Kontrolle drin, dieser sollte bei einem Produktivsystem entfernt werden.
Unser PHP-Script ist nun sendebereit.

Es folgt jetzt der Grundaufbau für die Firebase Realtime Database in PHP. Für die Ergänzung benötigen wir die Firebase Realtime Database Url und die Project ID.
Beide tragen wir in die index.php ein.

index.php

<?php 
    define(SERVER_KEY, "");             // euer Serverschlüssel
    define(TOPIC_ADRESS, "/topics/info");
    define(FIREBASE_SERVER_FCM, "https://fcm.googleapis.com/fcm/send");
    define(FIREBASE_SERVER_DATABASE, "https://{Prject ID}.firebaseio.com/");        // eure Project ID eingeben

In der utils.php ergänzen wir zwei Funktionen. Die Erste soll helfen unsere Datenbank abzufragen ob ein Feed schon gesendet wurde und die Zweite schreibt welcher Feed schon gesendet wurde.

utils.php

function url_get_contents ($url) {
    if (!function_exists('curl_init')){ 
        die('CURL is not installed!');
    }
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $output = curl_exec($ch);
    curl_close($ch);
    return $output;
}

function put($key, $value) {
    $firebase_server = FIREBASE_SERVER_DATABASE."feeds.json";
    $httpheader = array('Content-Type:application/json');
    $post_content = array($key => $value);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $firebase_server);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_content));
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

Die index.php muss, in der if-Abfrage, nun vervollständigt werden. In der Datenbank selber schreiben wir den Schlüssel aus dem md5-Wert und zur Kontrolle das Datum vom Feed. In der Variable $file_get wird der Rückgabewert zugewiesen, so das in einer weiteren if-Abfrage der Wert auf null geprüft werden kann. Wenn er null ist, wissen wir, dass der Feed noch nicht gesendet wurde.

index.php

foreach($items as $item) {
    if (substr($item['pubDate'], 0, strlen($item['pubDate'])-9) == date('d.m.Y')) {
        $file_get = url_get_contents(FIREBASE_SERVER_DATABASE."feeds/" . md5($item['title'].$item['pubDate']) . ".json");

        if ($file_get == "null") {
            echo send($item['title'], $item['creator'], $item['link']);
            put(md5($item['title'].$item['pubDate']), $item['pubDate']);
        }
    }
}

Die beiden Dateien sollten fertig so aussehen.

index.php

<?php 
    define(SERVER_KEY, "");
    define(TOPIC_ADRESS, "/topics/info");
    define(FIREBASE_SERVER_FCM, "https://fcm.googleapis.com/fcm/send");
    define(FIREBASE_SERVER_DATABASE, "https://{Projct ID}.firebaseio.com/");
    header("Content-type: application/json; charset=utf-8");

    $result_rss    = requireToVar('./rss.php');
    $items         = json_decode($result_rss, true)['item'];

    foreach($items as $item) {
        if (substr($item['pubDate'], 0, strlen($item['pubDate'])-9) == date('d.m.Y')) {
            $file_get = url_get_contents(FIREBASE_SERVER_DATABASE."feeds/" . md5($item['title'].$item['pubDate']) . ".json");

            if ($file_get == "null") {
                echo send($item['title'], $item['creator'], $item['link']);
                put(md5($item['title'].$item['pubDate']), $item['pubDate']);
            }
        }
    }

    function requireToVar($file){
        ob_start();
        require($file);
        return ob_get_clean();
    }
?>

utils.php

<?php

    function createDate($oldDate) {
        return DateTime::createFromFormat('D, d M Y H:i:s', substr($oldDate, 0, strlen($oldDate)-4))->format('d.m.Y H:i:s');
    }

    function url_get_contents ($url) {
        if (!function_exists('curl_init')){ 
            die('CURL is not installed!');
        }
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $output = curl_exec($ch);
        curl_close($ch);
        return $output;
    }

    function send($title, $creator, $link) {
        $post_content = array('to' => TOPIC_ADRESS, 'data' => array('title' => $title, "creator" => $creator, "link" => $link));
        $httpheader = array('Content-Type:application/json', 'Authorization:key='.SERVER_KEY);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, FIREBASE_SERVER_FCM);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_content));
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }

    function put($key, $value) {
        $firebase_server = FIREBASE_SERVER_DATABASE."feeds.json";
        $httpheader = array('Content-Type:application/json');
        $post_content = array($key => $value);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $firebase_server);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_content));
        $response = curl_exec($ch);
        curl_close($ch);
        return $response;
    }
?>

Im TEIL 6 haben wir die build.gradle vorbereitet. Jetzt fehlt uns, in der {project}/{app}/build.gradle Datei, noch die Library aufnahme von Firebase Cloud Messaging.
Ergänzt im Tag dependencies { … } und Synchronisiert mit Sync now

compile 'com.google.firebase:firebase-messaging:10.0.1'

Im nächsten Abschnitt werden wir uns mit der „Registrierung“ im Firebase-System beschäftigen. Viel Code ist es aber nicht
Firebase Cloud Messaging bietet dafür gute Klassen, was uns eine menge Arbeit spart. Wir benötigen ganze zwei Klassen zur Erweiterung, also ran an die Arbeit und fertig werden

Die erste ist die FireBase_Instance_ID_Service.java, diese erweitern wir mit FirebaseInstanceIdService. Durch die erstellte Klasse werden wir im Firebase-System registriert und zeigen Firebase Cloud Messaging, in seiner Instanz, welches Topic registriert werden soll. Den Topic haben wir in der index.php schon definiert. Beide müssen identisch sein. Dazu muss die onTokenRefresh-Methode überschrieben werden.

FireBase_Instance_ID_Service.java

public class FireBase_Instance_ID_Service extends FirebaseInstanceIdService {

    protected final static String TOPIC = "info";

    @Override
    public void onTokenRefresh() {
        super.onTokenRefresh();
        FirebaseMessaging.getInstance().subscribeToTopic(TOPIC);
    }
}

Nun benötigen wir noch die Empfangsklasse FireBase_Messaging_Service.java diese wird mit FirebaseMessagingService erweitert.
Was wollen wir? Eine Notification soll erscheinen, die uns dann durch daraufklicken in den Artkiel befördert.
Auch hier müssen wir eine Methode (onMessageReceived) überschreiben. In der Methode fangen wir die Nachricht vom Topic, aber, bevor wir jede Topic-Nachricht in unsere Notification einbringen prüfen wir mittels if-Abfrage ob es auch unsere ist die gebraucht wird.

FireBase_Messaging_Service.java

public class FireBase_Messaging_Service extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        if (remoteMessage.getFrom().equals("/topics/" + FireBase_Instance_ID_Service.TOPIC)) {
            Notification notification = new NotificationCompat.Builder(this)
                    .setContentTitle(String.format("von %s", remoteMessage.getData().get("creator")))
                    .setContentText(remoteMessage.getData().get("title"))
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setDefaults(Notification.DEFAULT_ALL)
                    .setAutoCancel(true)
                    .setStyle(new NotificationCompat.BigTextStyle().bigText(remoteMessage.getData().get("title")))
                    .build();
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse(remoteMessage.getData().get("link")));
            PendingIntent contentIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, 0);

            NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            notification.contentIntent = contentIntent;
            notificationManager.notify(getID(), notification);
        } 
    }

    private final static AtomicInteger c = new AtomicInteger(0);

    public static int getID() {
        return c.incrementAndGet();
    }
}

Der letzte Schritt! Der Eintrag in der AndroidManifest.xml

<service android:name=".FireBase_Messaging_Service">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

<service android:name=".FireBase_Instance_ID_Service">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
    </intent-filter>
</service>

Um eine automatisiertes Verfahren zu realisieren, verwenden wir einen Cronjob. Da nicht jeder Webhoster, das anbietet hab ich mal diesen hier gewählt -> https://cron-job.org/de/. Registriert euch und erstellt euch ein Cronjob. Die maximale Zeit für das ausführen des Scripts darf 30 Sekunden nicht überschreiten, daher werden wir unsere Textausgabe in der index.php entfernen.

Entfernt das „echo“ vor dem send(…); und setzt ein break, das bewirkt, dass immer nur ein Feed-Item angestoßen wird um einen Timeout aus dem Wege zu gehen. Probiert es euch aus.

aus

echo send($item['title'], $item['creator'], $item['link']);

wird

send($item['title'], $item['creator'], $item['link']);
        if ($file_get == "null") {
            send($item['title'], $item['creator'], $item['link']);
            put(md5($item['title'].$item['pubDate']), $item['pubDate']);
            break; // verhindert das ein weiters Item abgehandelt wird
        }

Das vollständige Projekt findet ihr hier.