В этой статье мы рассмотрим, как загружать классы (в том числе, фрагменты) из сети во время выполнения программы, и использовать их в своем Android-приложении. Область применения подобной технологии на практике — это отдельная тема для разговора, мне же сама по себе реализация данной функциональности показалась довольно интересной задачей.
Приступим.
Создаем фрагмент
Для начала создадим некий фрагмент Fragment0 и реализуем у него метод onCreateView():
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
//return inflater.inflate(R.layout.fragment1, container, false);
LinearLayout linearLayout = new LinearLayout(getActivity());
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
Button button = new Button(getActivity());
button.setText("Кнопка");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showFragment("jatx.networkingclassloader.dx.Fragment1", null); // рассмотрим чуть позже
}
});
linearLayout.addView(button, lp);
return linearLayout;
}
Стандартный метод создания разметки из xml в нашем случае работать не будет, поэтому для первого фрагмента мы создаем ее программно.
Далее нам нужно на основе модуля, содержащего фрагмент, создать APK, распаковать его с помощью unzip, и выложить файл classes.dex на сервер.
Реализуем загрузку классов
В отдельном модуле создадим класс NetworkingActivity и реализуем в нем следующие методы:
@Override
protected void onCreate(Bundle savedInstanceState) {
// ......
dataDir = getApplicationInfo().dataDir;
frameLayout = (FrameLayout) findViewById(R.id.main_frame);
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setMessage("Загружаем классы из сети");
progressDialog.show();
// Загружаем classes.dex с сервера, подробно рассматривать не будем:
DownloadTask downloadTask = new DownloadTask(this, dataDir);
downloadTask.execute(null, null, null);
// receiver нужен для того, чтобы мы могли из фрагмента открывать другие фрагменты:
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String className = intent.getStringExtra("className");
Bundle args = intent.getBundleExtra("args");
showFragment(className, args);
}
};
IntentFilter filter = new IntentFilter("jatx.networkingclassloader.ShowFragment");
registerReceiver(receiver, filter);
}
// Вызывается, когда наш AsyncTask успешно загрузил c сервера classes.dex:
public void downloadReady() {
Toast.makeText(this, "Классы из сети загружены", Toast.LENGTH_SHORT).show();
progressDialog.dismiss();
showFragment("jatx.networkingclassloader.dx.Fragment0", null);
}
public void showFragment(String className, Bundle arguments) {
// Наш загруженный файл:
File dexFile = new File(dataDir, "classes.dex");
Log.e("Networking activity", "Loading from dex: " + dexFile.getAbsolutePath());
// Каталог кэша, нужен для DexClassLoader:
File codeCacheDir = new File(getCacheDir() + File.separator + "codeCache");
codeCacheDir.mkdirs();
// Создаем ClassLoader:
DexClassLoader dexClassLoader = new DexClassLoader(
dexFile.getAbsolutePath(), codeCacheDir.getAbsolutePath(), null, getClassLoader());
try {
// Загружаем класс фрагмента по имени:
Class clazz = dexClassLoader.loadClass(className);
// Создаем объект класса:
Fragment fragment = (Fragment) clazz.newInstance();
// Передаем фрагменту аргументы и отображаем его:
fragment.setArguments(arguments);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.main_frame, fragment);
fragmentTransaction.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
Открываем из фрагмента другие фрагменты
Для этого в классе LoadableFragment (суперкласс всех наших фрагментов) реализуем следующий метод:
public void showFragment(String className, Bundle args) {
Intent intent = new Intent("jatx.networkingclassloader.ShowFragment");
intent.putExtra("className", className);
intent.putExtra("args", args);
getActivity().sendBroadcast(intent);
}
Надеюсь, здесь все понятно.
Наш следующий фрагмент мы попробуем создать несколько иначе.
Подгружаем из сети xml-разметку
Для начала, создаем и выкладываем на сервер файл разметки.
Я нашел на github библиотеку, которая умеет парсить xml layout из строки. Для корректной работы пришлось ее немного подпилить.
И так, добавим в наш класс LoadableFragment следующие методы:
protected void loadLayoutFromURL(FrameLayout container, String url) {
this.container = container;
// загружаем файл разметки:
LayoutDownloadTask layoutDownloadTask = new LayoutDownloadTask(this, url);
layoutDownloadTask.execute(null, null, null);
}
// Вызывается, если xml-разметка успешно загружена:
public void onLayoutDownloadSuccess(String xmlAsString) {}
Теперь с помощью этого всего создадим фрагмент Fragment1:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(getActivity());
loadLayoutFromURL(frameLayout, "http://tabatsky.ru/testing/fragment1.xml");
return frameLayout;
}
@Override
public void onLayoutDownloadSuccess(String xmlAsString) {
LinearLayout linearLayout = (LinearLayout) DynamicLayoutInflator.inflate(getActivity(), xmlAsString, container);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
linearLayout.setLayoutParams(lp);
final EditText editText = (EditText) DynamicLayoutInflator.findViewByIdString(linearLayout, "edit_text");
Button button = (Button) DynamicLayoutInflator.findViewByIdString(linearLayout, "button");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle args = new Bundle();
args.putString("userName", editText.getText().toString());
showFragment("jatx.networkingclassloader.dx.Fragment2", args);
}
});
}
Послесловие
Полностью исходный код проекта можно посмотреть на github.
Готовый APK можно скачать здесь.
Ну и напоследок, хочу сказать пару слов о возможном применении подобной технологии: например, можно выдавать с сервера разные classes.dex в зависимости от типа аккаунта пользователя (платный/бесплатный), что должно несколько увеличить сложность реверс-инжиниринга приложения.
Автор: Евгений Табацкий