понедельник, 5 октября 2015 г.

Callback'и в Android

Еще решил написать одну заметку по поводу коллбеков. Штука интересная и полезная, без нее редко когда получается что то разумное написать. По этому решил что надо будет написать пару примеров по работе с колбеками.


Для начала хочу сказать что есть разные библиотеки типа EventBus и OttoBus которые сделают всю работу за вас и еще больше сделают… Но иногда эти библиотеки сильно много делают для обычной тривиальной задачи, например вернуть респонс из AsyncTask'a или еще что-то похожее, из-за этого использовать эти библиотеки не целесообразно и достаточно написать один интерфейс и вызывать его в нужном месте по тому или иному событию. Собственно это я сегодня и хочу продемонстрировать на примере приложения.

У нас будет небольшой апп который будет тянуть список спорт. каналов через AsyncTask, парсить то что вернет сервер с помощью кастомного JsonArray и JsonObject и отдавать это все колбеком в активити для отображения в списке.

Для начала давайте сделаем небольшую настройку. Для работы с интернетом нам нужен премишен в манифесте, давайте его добавим.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="project.example.callback.dajver.callbacksexample" >

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>


И сделаем разметку с списком, все будет елементарно и просто.

activity_main.xml
<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"
    tools:context=".MainActivity"
    android:weightSum="1">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView" />
</LinearLayout>


Все, с настройками мы закончили, теперь давайте кодить!

Первый вариант Callback'a, получение данных в Activity

Первый вариант у нас будет такой как я описал выше, он имеет вот такую архитектуру:

// Интерфейс колбека
interface MyCallback {
    void callbackCall();
}

// Этот класс выполняет какое то действие
class Worker {
   MyCallback callback;

   // по этому событию у нас происходит вызов колбека
   void onEvent() {
      callback.callbackCall();
   }
}

//дальше в нашей активити обрабатываем этот колбек
class MainActivity extends Activity implements MyCallback {
   void callbackCall() {
      // что то происходит здесь
   }
}


Это очень краткий пример того что будет происходить. Создаем интерфейс, далее в классе который нам должен что-то вернуть вызываем этот колбек что бы можно было отследить когда он выполнился, а после этого отображаем то что вернул колбек в нашей активити которая включает в себя собственно этот колбек.

Очень сумбурно, нам нужно более детальное рассмотрение этого способа по этому начну.

У вас должен уже быть создан пустой проект с MainActivity, нам она пока не понадобится, мы ее будем использовать в в самом конце. 

Сейчас создайте новый класс который будет называться BackgroundTask, в этом методе у нас будет AsyncTask который у нас будет хватать json строку с удаленного сервера. 

Вообще использовать AsyncTask не советую, для таких нужд есть прекрасная библотека Retrofit или Volley или еще куча разных великолепных библиотек которые делают запросы и не валятся при повороте экрана. По этому это чисто пример, так как я сделал запрос лучше не делать, повторюсь, лучше используйте готовые решения.

BackgroundTask.java
import android.os.AsyncTask;

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

import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;
import project.example.callback.dajver.callbacksexample.rest.parser.ChannelsParser;
import project.example.callback.dajver.callbacksexample.rest.di.ResponseCallback;

public class BackgroundTask extends AsyncTask<ArrayList<ChannelsModel>, Void, ArrayList<ChannelsModel>> {

    //объявляем наш колбек
    private ResponseCallback callback;

    //в конструкторе присваеваем его
    public BackgroundTask(ResponseCallback callback) {
        this.callback = callback;
    }

     //дальше выполняем запрос на сервер
    @Override
    protected ArrayList<ChannelsModel> doInBackground(ArrayList<ChannelsModel>... params) {
        StringBuilder content = new StringBuilder();
        try {
            URL url = new URL("http://novaforen.com/reward/channels.json");
            URLConnection urlConnection = url.openConnection();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                content.append(line + "\n");
            }
            bufferedReader.close();
        } catch(Exception e) {
            e.printStackTrace();
        }
         //парсим и возвращаем наш ArrayList с данными в onPostExecute
        return new ChannelsParser().parseData(content.toString());
    }

    //а вот тут наш колбек возвращает нам результат в MainActivity
    @Override
    protected void onPostExecute(ArrayList<ChannelsModel> result) {
        callback.response(result);
    }
}


Вот как то так у нас будет работать наш AsyncTask, но пока что вам не хватает нескольких классов которые выполняют кое какую работу, давайте их напишем что бы все заработало. Нам не хватает интерфейса ResponseCallback, так давайте создадим его.

ResponseCallback.java
import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

public interface ResponseCallback {
    void response(ArrayList<ChannelsModel> response);
}


Так же нам не хватает ChannelsModel, давайте и его создадим.

ChannelsModel.java
public class ChannelsModel {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


А еще нам не хватает ChannelsParser который парсит нашу json возвращенную с сервера. Держите, мне не жалко!

ChannelsParser.java
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

public class ChannelsParser {

    public ArrayList<ChannelsModel> parseData(String response) {
        ArrayList<ChannelsModel> model = new ArrayList<>();
        try {
            JSONObject json = new JSONObject(new String(response));
            JSONArray channels = json.getJSONArray("channels");
            for(int i = 0; i < channels.length(); i++) {
                ChannelsModel gru = new ChannelsModel();
                JSONObject data = channels.getJSONObject(i);
                String name = data.getString("name");
                gru.setName(name);
                model.add(gru);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return model;
    }
}


Вот! Вроде бы картинка у нас обрисовалась и все нужные классы были созданы. Теперь нам нужно все это собрать в одно, отобразить наш заветный список на экране. Разметка у нас уже готова по-этому нам осталось сделать адаптер и вызвать его в MainActivity. Вот это мы сейчас и сделаем, создаем еще один класс ChannelsAdapter и заполняем его.

ChannelsAdapter.java
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

public class ChannelsAdapter extends ArrayAdapter<ChannelsModel> {
    private Context context;
    //наш список который мы будем получать из mainactivity
    private ArrayList<ChannelsModel> model;

    //конструктор, ну а куда же без него отдаем адаптеру контекст и спарсенные данные
    public ChannelsAdapter(Context context, ArrayList<ChannelsModel> values) {
        super(context, android.R.layout.simple_list_item_1, values);
        this.context = context;
        this.model = values;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View rowView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);

        //отображаем
        final TextView textView = (TextView) rowView.findViewById(android.R.id.text1);
        textView.setText(model.get(position).getName());
        return rowView;
    }
}


Так, тут все просто я думаю все сталкивались с кастомными адаптерами по этому сильно вникать не буду. Если же не сталкивались то у меня в блоге есть статья где я описываю создание кастомного адаптера. Вернемся к функциям, в адаптере мы принимаем ArrayList с данными и отображаем его в TextView который я беру из ресурсов android'a. Собственно и все. Давайте уже наконец закончим и вызовем это все в MainActivity.

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.adapter.ChannelsAdapter;
import project.example.callback.dajver.callbacksexample.rest.BackgroundTask;
import project.example.callback.dajver.callbacksexample.rest.di.ResponseCallback;
import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

//подключили ResponseCallback к классу, и теперь можем получать то что этот колбек принимает
public class MainActivity extends AppCompatActivity implements ResponseCallback {

    //объявляем нужные классы
    private ListView listView;
    private ChannelsAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView) findViewById(R.id.listView);
        //вызываем наш AsyncTask и передаем ему контект 
        //в котором есть ResponseCallback при помощи this
        new BackgroundTask(this).execute();
    }

     // в методе колбека создаем адаптер и отображаем список 
    @Override
    public void response(ArrayList<ChannelsModel> response) {
        adapter = new ChannelsAdapter(this, response);
        listView.setAdapter(adapter);
    }
}


Вот так вот просто на примере можно увидеть как работают коллбеки. Но это не все, еще есть второй вариант колбеков которые работают через Setters и Getters. Я на примере этой же программы покажу как это делается, там так же ничего сложного нет. 

Исходники
GitHub


Второй вариант Callback'a, работа через setters и getters

Как и с первым вариантом во втором я приведу абстрактный пример того как он должен выглядеть в кратце.

    //обявляем интерфейс
    private OnScoreSavedListener onScoreSavedListener;

    //создаем интерфейс с колбеком
    public interface OnScoreSavedListener {
        public void onScoreSaved();
    }

    //создаем сеттер для листенера
    public void setOnScoreSavedListener(OnScoreSavedListener listener) {
        onScoreSavedListener = listener;
    }

    //где то вызывается его метод для срабатывания
    onScoreSavedListener.onScoreSaved();

    //а дальше вызываем в активити
MyCustomView slider = (MyCustomView) view.findViewById(R.id.slider)
    slider.setOnScoreSavedListener(new OnScoreSavedListener() {
        @Override
        public void onScoreSaved() {
            Log.v("","EVENT FIRED");
        }
});


Интерфейс можно создавать где угодно и как угодно, по этому не обязательно его выносить в отдельный класс, можно создать как внутри активити или асинк таска так и внутри адаптера и любого другого класса. Создается интерфейс, объявляется, дальше вызывается его инстанс, то есть метод который должен отработать во время евента. А потом в Activity или Fragment'e идет вызов этого метода и получение нужных данных из него. В общем пример опять сумбурный, сейчас на примере нашей программы сделаем и будет все понятно.

Для начала в нашем адаптере нужно создать интерфейс. В самом низу класса пишем вот такое:

ChannelsAdapter.java
public OnAdapterClickListener listener;
//коллбек будет возвращать нам имя кликнутого айтема
public interface OnAdapterClickListener {
        void onCLick(String name);
}


Дальше нам надо создать setter для этого интерфейса что бы можно было вызывать его во время создания адаптера и передавать туда наш колбек.

ChannelsAdapter.java
public ChannelsAdapter setOnClickListener(OnAdapterClickListener onAdapterClickListener) {
        this.listener = onAdapterClickListener;
        return this;
}


Теперь в getView пишем onClickListener для создания евента клика по айтему:

ChannelsAdapter.java
textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //передаем имя
                listener.onCLick(textView.getText().toString());
            }
});


Вот так должен выглядеть адаптер в целом

ChannelsAdapter.java
package project.example.callback.dajver.callbacksexample.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

public class ChannelsAdapter extends ArrayAdapter<ChannelsModel> {
    private Context context;
    private ArrayList<ChannelsModel> model;
    public OnAdapterClickListener listener;

    public ChannelsAdapter(Context context, ArrayList<ChannelsModel> values) {
        super(context, android.R.layout.simple_list_item_1, values);
        this.context = context;
        this.model = values;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View rowView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);

        final TextView textView = (TextView) rowView.findViewById(android.R.id.text1);
        textView.setText(model.get(position).getName());
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                listener.onCLick(textView.getText().toString());
            }
        });
        return rowView;
    }

    public ChannelsAdapter setOnClickListener(OnAdapterClickListener onAdapterClickListener) {
        this.listener = onAdapterClickListener;
        return this;
    }

    public interface OnAdapterClickListener {
        void onCLick(String name);
    }
}


А теперь нам нужно перенести в MainActivity этот функционал. Для этого нам нужно присвоить адаптеру новый сеттер и передать туда новый объект нашего коллбека.

MainActivity.java
@Override
    public void response(ArrayList<ChannelsModel> response) {
        adapter = new ChannelsAdapter(this, response).setOnClickListener(new ChannelsAdapter.OnAdapterClickListener() {
            @Override
            public void onCLick(String name) {
                Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
                //здесь можно вызывать другу активити или запускать другой фрагмент
            }
        });
        listView.setAdapter(adapter);
}


В целом MainActivity должна выглядить таким образом

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;

import project.example.callback.dajver.callbacksexample.adapter.ChannelsAdapter;
import project.example.callback.dajver.callbacksexample.rest.BackgroundTask;
import project.example.callback.dajver.callbacksexample.rest.di.ResponseCallback;
import project.example.callback.dajver.callbacksexample.rest.parser.model.ChannelsModel;

public class MainActivity extends AppCompatActivity implements ResponseCallback {

    private ListView listView;
    private ChannelsAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView) findViewById(R.id.listView);
        new BackgroundTask(this).execute();
    }

    @Override
    public void response(ArrayList<ChannelsModel> response) {
        adapter = new ChannelsAdapter(this, response).setOnClickListener(new ChannelsAdapter.OnAdapterClickListener() {
            @Override
            public void onCLick(String name) {
                Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
                //здесь можно вызывать другу активити или запускать другой фрагмент
            }
        });
        listView.setAdapter(adapter);
    }
}


Вот так вот при использовании коллбеков можно достичь дзена. Надеюсь все было понятно так как пока я это писал меня двадцать пять раз отвлекли и я все эти разы терял мысль по этому возможно что то может быть черезчур запутано, спрашивайте если что то не понятно. В любом случае есть исходники рабочие, по ним так точно можно разобраться (:

Исходники
GitHub

2 комментария:

  1. Первый пример, смысл использовать AsyncTask не в Активити, используем его в Активити и в методе onPostExecute() обновляем View, Callback'и не нужны будут.

    ОтветитьУдалить
    Ответы
    1. Ну вообще нежелательно использовать классы внутри классов. Логику нужно всегда разделять, и вот как раз в этом случае нужны коллбеки которые будут возвращать результат. Советую почитать книгу Clean Code и почитать про SOLID

      Удалить