В этой статье мы рассмотрим, как загружать классы (в том числе, фрагменты) из сети во время выполнения программы, и использовать их в своем Android-приложении. Область применения подобной технологии на практике — это отдельная тем�� для разговора, мне же сама по себе реализация данной функциональности показалась довольно интересной задачей.
Приступим.
Для начала создадим некий фрагмент Fragment0 и реализуем у него метод onCreateView():
Стандартный метод создания разметки из xml в нашем случае работать не будет, поэтому для первого фрагмента мы создаем ее программно.
Далее нам нужно на основе модуля, содержащего фрагмент, создать APK, распаковать его с помощью unzip, и выложить файл classes.dex на сервер.
В отдельном модуле создадим класс NetworkingActivity и реализуем в нем следующие методы:
Для этого в классе LoadableFragment (суперкласс всех наших фрагменто��) реализуем следующий метод:
Надеюсь, здесь все понятно.
Наш следующий фрагмент мы попробуем создать несколько иначе.
Для начала, создаем и выкладываем на сервер файл разметки. Я нашел на github библиотеку, которая умеет парсить xml layout из строки. Для корректной работы пришлось ее немного подпилить.
И так, добавим в наш класс LoadableFragment следующие методы:
Теперь с помощью этого всего создадим фрагмент Fragment1:
Полностью исходный код проекта можно посмотреть на github. Готовый APK можно скачать здесь.
Ну и напоследок, хочу сказать пару слов о возможном применении подобной технологии: например, можно выдавать с сервера разные classes.dex в зависимости от типа аккаунта пользователя (платный/бесплатный), что должно несколько увеличить сложность реверс-инжиниринга приложения.
Приступим.
Создаем фрагмент
Для начала создадим некий фрагмент 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 в зависимости от типа аккаунта пользователя (платный/бесплатный), что должно несколько увеличить сложность реверс-инжиниринга приложения.
