Android: динамически подгружаем фрагменты из сети

в 0:43, , рубрики: android development, classloader, java, networking, Разработка под android

В этой статье мы рассмотрим, как загружать классы (в том числе, фрагменты) из сети во время выполнения программы, и использовать их в своем 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 в зависимости от типа аккаунта пользователя (платный/бесплатный), что должно несколько увеличить сложность реверс-инжиниринга приложения.

Автор: Евгений Табацкий

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js