Обробка кнопки повернення в компоненті навігації Android


83

Я хотів би знати, як правильно обробляти дії кнопки повернення системи за допомогою навігаційного контролера. У моєму додатку у мене є два фрагменти (наприклад, фрагмент1 та фрагмент2), і я маю дію у фрагменті1 з призначенням до фрагмента2. Все працює добре, крім одного - коли користувач натискає кнопку повернення системи у фрагменті2, я хочу показати діалогове вікно (наприклад, використовуючи DialogFragment) для підтвердження виходу. Який найкращий спосіб реалізувати таку поведінку? Якщо я використовую app:defaultNavHost="true"у своєму фрагменті хосту, він автоматично повертається назад, ігноруючи мої правила. І, крім того, для чого цей компонент?

введіть тут опис зображення

Чи варто використовувати "pop to"?


1
За допомогою "Поп до" ви можете визначити, куди йти (пункт призначення), натискаючи кнопку резервного копіювання.
Алекс

@Alex Отже, якщо для нього встановлено значення none, як він повинен реагувати на кнопку повернення?
Кирил Ткач,

1
Коли для нього встановлено значення "немає", поведінка за замовчуванням, користувач буде переведений до попереднього пункту призначення (фрагмент 1)
Олексій

@Alex, гаразд, чи є спосіб обробити кнопку назад за другим фрагментом?
Кирил Ткач,

Відповіді:


146

Останнє оновлення - 25 квітня 2019 р

Новий випуск androidx.activity ver. 1.0.0-alpha07 вносить деякі зміни

Більше пояснень в офіційному посібнику для Android: Забезпечте користувальницьку зворотну навігацію

Приклад:

public class MyFragment extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // This callback will only be called when MyFragment is at least Started.
        OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
            @Override
            public void handleOnBackPressed() {
                // Handle the back button event
            }
        };
        requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);

        // The callback can be enabled or disabled here or in handleOnBackPressed()
    }
    ...
}

Старі оновлення

UPD: 3 квітня 2019 р

Тепер це спрощено. Більше інформації тут

Приклад:

requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), this);

@Override
public boolean handleOnBackPressed() {
    //Do your job here
    //use next line if you just need navigate up
    //NavHostFragment.findNavController(this).navigateUp(); 
    //Log.e(getClass().getSimpleName(), "handleOnBackPressed");
    return true;
    }

Не підтримується (з версії 1.0.0-alpha06, 3 квітня 2019 р.):

Так як це , вона може бути реалізована тільки з допомогою JetPack реалізації OnBackPressedCallbackв вашому фрагменті і додати його до діяльності: getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);

Ваш фрагмент повинен виглядати так:

public MyFragment extends Fragment implements OnBackPressedCallback {

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);
}
    
    @Override
    public boolean handleOnBackPressed() {
        //Do your job here
        //use next line if you just need navigate up
        //NavHostFragment.findNavController(this).navigateUp(); 
        //Log.e(getClass().getSimpleName(), "handleOnBackPressed");
        return true;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        getActivity().removeOnBackPressedCallback(this);
    }
}

UPD: Ваша активність повинна розширюватися AppCompatActivityабо FragmentActivityу файлі Gradle:

 implementation 'androidx.appcompat:appcompat:{lastVersion}'

4
Чи існує це ще в Jetpack?
Bryan Bryce

1
Якщо ваша активність AppCompat не з підтримки lib, а з androidX lib, тоді так, ви повинні
Jurij Pitulja

3
Використовуючи AndroidX AppCompatActivity та Androidx Fragment, і я теж не бачу цього як доступний варіант
Omega142

3
OnBackPressCallback - це абстрактний клас зараз! Тож це рішення для мене не працює.
Джунія Монтана

3
Як зробити звичайну операцію задньої панелі після виконання деякої логіки на - handleIOnBackPress ()?
krishnakumarcn

22

Отже, я створив інтерфейс

public interface OnBackPressedListener {
    void onBackPressed();
}

І реалізували це усі фрагменти, які повинні обробляти кнопку повернення. В основній діяльності я замінив onBackPressed()метод:

@Override
public void onBackPressed() {
    final Fragment currentFragment = mNavHostFragment.getChildFragmentManager().getFragments().get(0);
    final NavController controller = Navigation.findNavController(this, R.id.nav_host_fragment);
    if (currentFragment instanceof OnBackPressedListener)
        ((OnBackPressedListener) currentFragment).onBackPressed();
    else if (!controller.popBackStack())
        finish();

}

Отже, якщо верхній фрагмент мого хосту навігації реалізує OnBackPressedListenerінтерфейс, я викликаю його onBackPressed()метод, в іншому випадку я просто вивожу назад стек і закриваю програму, якщо задній стек порожній.


22
Гідне рішення. Хоча мені завжди цікаво, в чому сенс Навігаційного компонента, якщо йому не вистачає такої функціональності нестандартно.
First_Strike

Перевірте відповідь @Jurij Pitulja - це рекомендований спосіб вирішити проблему
Мітч,

Навіщо нам потрібен окремий інтерфейс замість того, щоб використовувати те, що є в Jetpack?
ІгорГанапольський

@IgorGanapolsky на момент цієї відповіді в Jetpack не було інтерфейсу.
Кирил Ткач,

@KirylTkach що таке декларація mNavHostFragment?
E.Akio

16

Рекомендуються підхід полягає в додаванні OnBackPressedCallbackдо даного виду діяльності OnBackPressedDispatcher.

requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { 
    // handle back event
}

1
краще, якщо власник зворотного виклику також передається таким чином .addCallback(viewLifecycleOwner) {}, інакше ви продовжуватимете отримувати зворотні дзвінки навіть після того, як фрагмент буде знищений.
Марат

13

Для тих, хто шукає реалізацію Kotlin, дивіться нижче.

Зверніть увагу, що OnBackPressedCallback єдиний, здається, працює для забезпечення користувацької поведінки назад для вбудованої програмної / апаратної кнопки повернення, а не кнопки зі стрілкою назад / додому як кнопки вгору на панелі дій / панелі інструментів. Щоб також замінити поведінку кнопки назад на панелі дій / панелі інструментів, я надаю рішення, яке працює для мене. Якщо це помилка або ви знаєте найкраще рішення для цього випадку, будь ласка, коментуйте.

build.gradle

...
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.navigation:navigation-fragment-ktx:2.0.0"
implementation "androidx.navigation:navigation-ui-ktx:2.0.0"
...

MainActivity.kt

...
import androidx.appcompat.app.AppCompatActivity
...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        setContentView(R.layout.activity_main)

        ...

        val navController = findNavController(R.id.nav_host_fragment)
        val appBarConfiguration = AppBarConfiguration(navController.graph)

        // This line is only necessary if using the default action bar.
        setupActionBarWithNavController(navController, appBarConfiguration)

        // This remaining block is only necessary if using a Toolbar from your layout.
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        toolbar.setupWithNavController(navController, appBarConfiguration)
        // This will handle back actions initiated by the the back arrow 
        // at the start of the toolbar.
        toolbar.setNavigationOnClickListener {
            // Handle the back button event and return to override 
            // the default behavior the same way as the OnBackPressedCallback.
            // TODO(reason: handle custom back behavior here if desired.)

            // If no custom behavior was handled perform the default action.
            navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
        }
    }

    /**
     * If using the default action bar this must be overridden.
     * This will handle back actions initiated by the the back arrow 
     * at the start of the action bar.
     */
    override fun onSupportNavigateUp(): Boolean {
        // Handle the back button event and return true to override 
        // the default behavior the same way as the OnBackPressedCallback.
        // TODO(reason: handle custom back behavior here if desired.)

        // If no custom behavior was handled perform the default action.
        val navController = findNavController(R.id.nav_host_fragment)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}

MyFragment.kt

...
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
...

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val onBackPressedCallback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // Handle the back button event
            }
        }
        requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback)
    }
}

Офіційну документацію можна переглянути за адресою https://developer.android.com/guide/navigation/navigation-custom-back


1
Це найкраща відповідь. Ви повинні мати можливість обробляти зворотне натискання з панелі інструментів та звичайний зворотний тиск. Великі пальці вгору
odifek

Це мені дуже допомогло! Кнопка "Назад" на панелі інструментів не спрацьовувала, OnBackPressedCallbackале після додавання toolbar.setNavigationOnClickListener { onBackPressed() }вона спрацювала так, що тепер апаратне забезпечення назад і панель інструментів назад працюють однаково. Дякуємо за чітку детальну відповідь!
poby

Це правильне і найбільш логічне рішення. Напевно витримає перевірку часом.
Роуленд Мтетезі,

Я використовував лише код для перезаписування фрагмента, він працює чудово!
sud007

10

Ось рішення, яке має робити те, що ви хочете, але я вважаю це поганим рішенням, оскільки воно суперечить ідеї компонента навігації Android (дозволяючи андроїду обробляти навігацію).

Перевизначте "onBackPress" усередині своєї діяльності

override fun onBackPressed() {
    when(NavHostFragment.findNavController(nav_host_fragment).currentDestination.id) {
        R.id.fragment2-> {
            val dialog=AlertDialog.Builder(this).setMessage("Hello").setPositiveButton("Ok", DialogInterface.OnClickListener { dialogInterface, i ->
                finish()
            }).show()
        }
        else -> {
            super.onBackPressed()
        }
    }
} 

1
Дякую за ваше рішення, але я використав це, перш ніж вирішив перейти на компонент Навігація. Здається, це занадто рано :(
Кирил Ткач,

ще рано. я застосував аналогічне хакейне рішення для обробки навігації на панелі
додатків

9

Я писав в основній діяльності, як це,

override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.my_nav_host_fragment).navigateUp(appBarConfiguration)
    }   

6

В 2.1.0-alpha06

Якщо ви хочете обробляти зворотний тиск лише у поточному фрагменті

requireActivity().onBackPressedDispatcher.addCallback(this@LoginFragment) {
    // handle back event
}

На всю діяльність

requireActivity().onBackPressedDispatcher.addCallback() {
    // handle back event
}

6

Це 2 рядки коду, які можна прослуховувати для зворотного друку, з фрагментів, [ТЕСТОВАНО та РОБОЧЕ]

  requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {

            //setEnabled(false); // call this to disable listener
            //remove(); // call to remove listener
            //Toast.makeText(getContext(), "Listing for back press from this fragment", Toast.LENGTH_SHORT).show();
     }

5

Трохи пізно на вечірку, але з останнім випуском Navigation Component 1.0.0-alpha09, тепер у нас є AppBarConfiguration.OnNavigateUpListener.

Для отримання додаткової інформації зверніться до цих посилань: https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener https://developer.android.com/jetpack/docs/release-notes


Дякуємо, що показали мені примітки до випуску! З’ясувалося, що android: menuCategory = "secondary" уникає появи заднього стека!
Валентин Гавран

для мене це працює лише з панеллю інструментів, але не з кнопкою "Назад"
Юрій Пітуля

3

Рекомендований метод працював у мене, але після оновлення бібліотеки implementation 'androidx.appcompat:appcompat:1.1.0'

Реалізуйте, як показано нижче

 val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Handle the back button event
        }
    }
    requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)

за допомогою Котліна


2

Ви можете забезпечити власну зворотну навігацію за допомогою OnBackPressDispatcher

class MyFragment : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // This callback will only be called when MyFragment is at least Started.
    val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
        // Handle the back button event
// and if you want to need navigate up
//NavHostFragment.findNavController(this).navigateUp()
    }

    // The callback can be enabled or disabled here or in the lambda
}
}

Більше пояснень в офіційному посібнику для Android: https://developer.android.com/guide/navigation/navigation-custom-back


1

Якщо ви використовуєте компонент навігації, дотримуйтесь наведених нижче кодів у методі onCreateView () (у цьому прикладі я хочу просто закрити свою програму цим фрагментом)

 OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {
            new AlertDialog.Builder(Objects.requireNonNull(getActivity()))
                    .setIcon(R.drawable.icon_01)
                    .setTitle(getResources().getString(R.string.close_app_title))
                    .setMessage(getResources().getString(R.string.close_app_message))
                    .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            getActivity().finish();
                        }
                    })
                    .setNegativeButton(R.string.no, null)
                    .show();
        }
    };
    requireActivity().getOnBackPressedDispatcher().addCallback(this, backPressedCallback);

1

І якщо ви хочете мати таку ж поведінку і для кнопки повернення панелі інструментів, просто додайте це у свою активність:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == android.R.id.home) {
        getOnBackPressedDispatcher().onBackPressed();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

1

Використовуйте це, якщо ви використовуєте фрагмент, або додайте його в прослуховувач натискання кнопки. Це працює для мене.

requireActivity().onBackPressed()

Викликається, коли діяльність виявляє натискання користувачем клавіші "Назад". GetOnBackPressDispatcher () OnBackPressDispatcher} отримає шанс обробити кнопку "Назад" до того, як буде викликано поведінку android.app.Activity # onBackPress ()} за замовчуванням.


1

Просто додайте ці рядки

     override fun onBackPressed() {
            if(navController.popBackStack().not()) {
            //Last fragment: Do your operation here 
            finish()
   } 

navController.popBackStack () просто виведе ваш фрагмент, якщо це не ваш останній фрагмент


0

Ось моє рішення

Використовуйте androidx.appcompat.app.AppCompatActivityдля діяльності, яка містить NavHostFragmentфрагмент.

Визначте наступний інтерфейс та впровадіть його у всі фрагменти призначення навігації

interface InterceptionInterface {

    fun onNavigationUp(): Boolean
    fun onBackPressed(): Boolean
}

У своїй діяльності перевизначте onSupportNavigateUpта onBackPressed:

override fun onSupportNavigateUp(): Boolean {
        return getCurrentNavDest().onNavigationUp() || navigation_host_fragment.findNavController().navigateUp()
}

override fun onBackPressed() {
        if (!getCurrentNavDest().onBackPressed()){
            super.onBackPressed()
        }
}

private fun getCurrentNavDest(): InterceptionInterface {
        val currentFragment = navigation_host_fragment.childFragmentManager.primaryNavigationFragment as InterceptionInterface
        return currentFragment
}

Це рішення має ту перевагу, що фрагментам пунктів навігації не потрібно турбуватися про скасування реєстрації своїх слухачів, як тільки вони від'єднаються.


0

Я спробував рішення Юрія Пітулі, але мені просто не вдалося знайти getOnBackPressDispatcher або addOnBackPressCallback, також використовуючи рішення Кірила Ткача, не вдалося знайти поточний фрагмент, тому ось мій:

interface OnBackPressedListener {
    fun onBackPressed(): Boolean
}

override fun onBackPressed() {
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
    val currentFragment = navHostFragment?.childFragmentManager!!.fragments[0]
    if (currentFragment !is OnBackPressedListener || !(currentFragment as OnBackPressedListener).onBackPressed()) super.onBackPressed()

таким чином, ви можете фрагментарно вирішити, чи повинна дія взяти під контроль тиск назад чи ні.

Крім того, у вас є BaseActivity для всіх ваших дій, ви можете реалізувати так

override fun onBackPressed() {
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
    if (navHostFragment != null){
        val currentFragment = navHostFragment.childFragmentManager.fragments[0]
        if (currentFragment !is AuthContract.OnBackPressedListener ||
                !(currentFragment as AuthContract.OnBackPressedListener).onBackPressed()) super.onBackPressed()
    } else {
        super.onBackPressed()
    }
}

0

Якщо ви використовуєте BaseFragment для своєї програми, тоді ви можете додати onBackPressDispatcher до свого базового фрагмента.

//Make a BaseFragment for all your fragments
abstract class BaseFragment : Fragment() {

private lateinit var callback: OnBackPressedCallback

/**
 * SetBackButtonDispatcher in OnCreate
 */

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setBackButtonDispatcher()
}

/**
 * Adding BackButtonDispatcher callback to activity
 */
private fun setBackButtonDispatcher() {
    callback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            onBackPressed()
        }
    }
    requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}

/**
 * Override this method into your fragment to handleBackButton
 */
  open fun onBackPressed() {
  }

}

Перевизначте onBackPress () у своєму фрагменті, розширивши базовий фрагмент

//How to use this into your fragment
class MyFragment() : BaseFragment(){

private lateinit var mView: View

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    mView = inflater.inflate(R.layout.fragment_my, container, false)
    return mView.rootView
}

override fun onBackPressed() {
    //Write your code here on back pressed.
}

}


0

Залежно від вашої логіки, якщо ви хочете закрити лише поточний фрагмент, вам потрібно передати viewLifecycleOwner, код показано нижче:

   requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            requireActivity().finish()
        }
    })

Однак, якщо ви хочете закрити програму на backPress, незалежно від того, з якого фрагмента (можливо, ви цього не хотіли б!), Не передавайте viewLifecycleOwner. Також якщо ви хочете відключити кнопку "Назад", нічого не робіть всередині handleOnBackPress (), див. Нижче:

 requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // do nothing it will disable the back button
        }
    })

0

Спробуйте це. Думаю, це вам допоможе.

override fun onBackPressed() {
    when (mNavController.getCurrentDestination()!!.getId()) {

        R.id.loginFragment -> {
            onWarningAlertDialog(this, "Alert", "Do you want to close this application ?")
        }
        R.id.registerFragment -> {
            super.onBackPressed()
        }
    }
}



private fun onWarningAlertDialog(mainActivity: MainActivity, s: String, s1: String) {

        val dialogBuilder = AlertDialog.Builder(this)
        dialogBuilder.setMessage(/*""*/s1)
                .setCancelable(false)
                .setPositiveButton("Proceed", DialogInterface.OnClickListener { dialog, id ->
                    finish()
                })
                .setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, id ->
                    dialog.cancel()
                })

        // create dialog box
        val alert = dialogBuilder.create()
        // set title for alert dialog box
        alert.setTitle("AlertDialogExample")
        // show alert dialog
        alert.show()
    }

0

Я шукав багато тем, і жоден з них не працює. Нарешті я знайшов:

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toolbar mToolbar = findViewById(R.id.topAppBar);
    setSupportActionBar(mToolbar);
}

@Override
public boolean onSupportNavigateUp() {
    navController.navigateUp();
    return super.onSupportNavigateUp();
}

MyFragment.java

@Override
public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
    Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
    mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Do something when uses presses back button (showing modals, messages,...)
            // Note that this will override behaviour of back button
        }
    });
}

@Override
public void onStop() {
    // Reset back button to default behaviour when we leave this fragment
    Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
    mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mainActivity.onBackPressed();
        }
    });

    super.onStop();
}

0

просто створіть функцію розширення для фрагмента

fun Fragment.onBackPressedAction(action: () -> Boolean) {
    requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
        OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            this.isEnabled = action()
            if (!this.isEnabled) {
                requireActivity().onBackPressed()
            }
        }
    })
}

а після у фрагменті помістіть код у onCreateView (дія повинна повернути false, щоб викликати активність onBackPress)

onBackPressedAction { //do something }

0

Мені потрібно підтримувати як справжню кнопку "Назад", так і кнопку "Назад" на панелі інструментів з можливістю перевизначення клацання "Назад" в обох випадках (щоб показати діалогове вікно або щось інше). Я зробив додатковий метод в активності та відповідні логічні перевірки ('onBackPress' у моєму випадку) у фрагментах:

// Process hardware Back button
override fun onBackPressed() {
    if (canCloseActivity()) {
        super.onBackPressed()
    }
}

// Process toobar Back and Menu button
override fun onSupportNavigateUp(): Boolean {
    if (canCloseActivity()) {
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
    return false
}

// Do real check if has unfinished tasks, return false to override activity closing
private fun canCloseActivity(): Boolean {
    val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment

    return when {
        currentFragment is MyFragment && currentFragment.onBackPressed() -> false

        drawerLayout.isOpen -> {
            drawerLayout.close()
            false
        }
        fullScreenPreviewLayout.visibility == View.VISIBLE -> {
            closeFullscreenPreview()
            false
        }
        else -> true
    }
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.