В последней части мы разберем самый важный вопрос - передача данных между фрагментами, а также обновление адаптера при изменении списка фрагментов. В арсенале фрагментов нет достаточно удобного механизма startActivityForResult()
, поэтому разработчики предоставляют несколько удобных и не очень вариантов передачи данных между фрагментами.
Подробно вопрос передачи данных между фрагментами вы можете найти в документации .
Мы рассмотрим самый удобный и самый "правильный" способ - использование таких составляющих Android Architecture Components как ViewModel
и LiveData
.
Android Architecture Components - ряд вспомогательных библиотек, которые помогают реализовать паттерн MVVM, а также призваны помочь с проектированием, тестированием и сопровождением приложений.
Компонент ViewModel
- предназначен для хранения и управления данных, связанными с представлением, а также избавить от проблем, связанных с пересозданием activity во время таких операций, как поворот экрана и так далее. Более подробно мы познакомимся с этим компонентом позже, а пока нам надо знать, что мы будем хранить список контактов во ViewModel
и получать доступ к объекту ViewModel
из фрагментов.
Сначала создадим класс ViewModel
. Обратите внимание, что наш класс наследуется от класса ViewModel
. Создадим методы для добавления и удаления контакта, а также для геттер, который возвращает список с данными.
Copy public class DataViewModel extends ViewModel {
private final MutableLiveData < List < Contact >> data = new MutableLiveData <>();
public DataViewModel () {
data . setValue ( new ArrayList <>());
}
public MutableLiveData < List < Contact >> getData () {
return data;
}
public void addContact ( Contact contact) {
List < Contact > list = data . getValue ();
list . add (contact);
data . setValue (list);
}
public void removeContact ( int index) {
List < Contact > list = data . getValue ();
list . remove (index);
data . setValue (list);
}
}
Чтобы понять код, приведенный выше, нам необходимо разобраться с еще одним используемым компонентом - LiveData
.
Компонент LiveData
- предназначен для хранения объекта и разрешает подписаться на его изменения. Класс MutableLiveData
является расширением LiveData
, с отличием в том, что это не абстрактный класс и у него доступны методы postValue()
и setValue()
, которые позволяют менять значение LiveData.
Если говорить проще, то LiveData
- это обертка над данными, которая позволяет "подписаться" на обновление данных. Этим свойством мы и воспользуемся - "подпишемся" на обновление списка внутри ContactsFragment
и будем обновлять адаптер при обновлении списка с контактами.
Добавление нового контакта
Для начала реализуем логику добавления нового контакта в фрагментe AddContactFragment
. Сначала в методе onViewCreated
получим доступ к объекту ViewModel
. Мы сами не создаем объект, а получаем его из класса ViewModelProvider
.
Copy public class AddContactFragment extends Fragment {
private DataViewModel viewModel;
...
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
...
}
}
Далее, при обработке нажатия кнопки ADD , формируем объект класса Contact
и вызываем метод addContact()
у объекта DataViewModel
, в который передаем объект нового контакта.
Copy public class AddContactFragment extends Fragment {
private DataViewModel viewModel;
...
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
...
Button addContactButton = view . findViewById ( R . id . button_add );
addContactButton . setOnClickListener (v -> {
if (uri == null ) {
Toast . makeText ( requireContext() , "Contact image not set!" , Toast . LENGTH_SHORT ) . show ();
return ;
}
String name = nameEditText . getText () . toString ();
String email = EmailEditText . getText () . toString ();
String phone = PhoneEditText . getText () . toString ();
// Добавляем новый контакт в список
Contact contact = new Contact(name , email , phone , uri) ;
viewModel . addContact (contact);
getActivity() . onBackPressed ();
});
}
}
Обновление адаптера и удаление контакта
Теперь перейдем в класс ContactsFragment
. Для начала также получим ссылку на объект DataViewModel
Copy public class ContactsFragment extends Fragment implements ContactsAdapter . DeleteItemListener {
private DataViewModel viewModel;
...
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
}
}
Теперь мы должны "подписаться" на обновление MutableLiveData<List<Contact>> data
, который находится внутри объекта DataViewModel
.
Copy public class ContactsFragment extends Fragment implements ContactsAdapter . DeleteItemListener {
private DataViewModel viewModel;
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
viewModel . getData () . observe ( getViewLifecycleOwner() , contacts -> {
adapter . notifyDataSetChanged ();
});
...
}
}
С помощью метода getData()
мы получаем доступ к объекту data, вызываем метод observe, куда передаем ссылку на сам фрагмент (концепция LifecycleOwner будет рассмотрена позже) с помощью метода getViewLifecycleOwner()
, а также передаем метод с помощью лямбда-выражения. В методе мы описываем код, который будет выполнен, когда data будет обновлена. В нашем случае, мы обновляем адаптер и список с контактами будет обновлен.
Последний штрих - удаление контакта. Для этого отредактируем метод onDeleteItem()
Copy public class ContactsFragment extends Fragment implements ContactsAdapter . DeleteItemListener {
private DataViewModel viewModel;
...
@ Override
public void onDeleteItem ( int position) {
viewModel . removeContact (position);
}
}
Ниже приведен полный листинг классов
MainActivity.java ContactsFragment.java AddContactFragment.java DataViewModel.java ContactsAdapter.java Contact.java
Copy public class MainActivity extends AppCompatActivity {
@ Override
protected void onCreate ( Bundle savedInstanceState) {
super . onCreate (savedInstanceState);
setContentView( R . layout . activity_main ) ;
setWindow() ;
getSupportFragmentManager()
. beginTransaction ()
. setReorderingAllowed ( true )
. add ( R . id . container , ContactsFragment . class , null )
. addToBackStack ( "main" )
. commit ();
}
@ Override
public void onBackPressed (){
FragmentManager fm = getSupportFragmentManager() ;
if ( fm . getBackStackEntryCount () > 1 ) {
fm . popBackStack ();
} else {
finish() ;
}
}
private void setWindow () {
// Метод устанавливает StatusBar в цвет фона
Window window = this . getWindow ();
window . clearFlags ( WindowManager . LayoutParams . FLAG_TRANSLUCENT_STATUS );
window . addFlags ( WindowManager . LayoutParams . FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS );
window . setStatusBarColor ( getColor( R . color . activity_background ) );
View decor = getWindow() . getDecorView ();
decor . setSystemUiVisibility ( View . SYSTEM_UI_FLAG_LIGHT_STATUS_BAR );
}
private static final int REQUEST_EXTERNAL_STORAGE = 1 ;
private static final String [] PERMISSIONS_STORAGE = {
Manifest . permission . READ_EXTERNAL_STORAGE ,
Manifest . permission . WRITE_EXTERNAL_STORAGE
};
public void verifyStoragePermissions () {
// Проверяем наличие разрешения на запись во внешнее хранилище
int permission = ActivityCompat . checkSelfPermission ( this , Manifest . permission . WRITE_EXTERNAL_STORAGE );
if (permission != PackageManager . PERMISSION_GRANTED ) {
// Запрашиваем разрешение у пользователя
ActivityCompat . requestPermissions (
this ,
PERMISSIONS_STORAGE ,
REQUEST_EXTERNAL_STORAGE
);
}
}
}
Copy public class ContactsFragment extends Fragment implements ContactsAdapter . DeleteItemListener {
private DataViewModel viewModel;
private RecyclerView mRecyclerView;
private ContactsAdapter adapter;
private List < Contact > list = new ArrayList <>();
public ContactsFragment () {
// Required empty public constructor
}
@ Override
public View onCreateView ( LayoutInflater inflater , ViewGroup container ,
Bundle savedInstanceState) {
return inflater . inflate ( R . layout . fragment_contacts , container , false );
}
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
viewModel . getData () . observe ( getViewLifecycleOwner() , contacts -> {
adapter . notifyDataSetChanged ();
});
mRecyclerView = view . findViewById ( R . id . list );
mRecyclerView . setLayoutManager ( new LinearLayoutManager(requireContext()) );
adapter = new ContactsAdapter(requireContext() , viewModel . getData() . getValue() , this ) ;
mRecyclerView . setAdapter (adapter);
view . findViewById ( R . id . fab ) . setOnClickListener (v -> {
requireActivity() . getSupportFragmentManager ()
. beginTransaction ()
. setReorderingAllowed ( true )
. add ( R . id . container , AddContactFragment . class , null )
. addToBackStack ( "add_contact" )
. commit ();
});
}
@ Override
public void onDeleteItem ( int position) {
viewModel . removeContact (position);
}
}
Copy public class AddContactFragment extends Fragment {
private DataViewModel viewModel;
private Uri uri;
private ImageView mContactImage;
private static final int IMAGE_CAPTURE_REQUEST_CODE = 7777 ;
public AddContactFragment () {
// Required empty public constructor
}
@ Override
public View onCreateView ( LayoutInflater inflater , ViewGroup container ,
Bundle savedInstanceState) {
return inflater . inflate ( R . layout . fragment_add_contact , container , false );
}
@ Override
public void onActivityResult ( int requestCode , int resultCode , @ Nullable Intent data) {
super . onActivityResult (requestCode , resultCode , data);
if (requestCode == IMAGE_CAPTURE_REQUEST_CODE && resultCode == Activity . RESULT_OK && data != null ) {
Bundle extras = data . getExtras ();
Bitmap imageBitmap = (Bitmap) extras . get ( "data" );
// Меняем ImageView с изображением контакта
mContactImage . setImageBitmap (imageBitmap);
String filename = "contact_" + System . currentTimeMillis () + ".png" ;
File outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), filename);
FileOutputStream fileOutputStream = null ;
try {
fileOutputStream = new FileOutputStream(outputFile) ;
imageBitmap . compress ( Bitmap . CompressFormat . PNG , 100 , fileOutputStream);
// Сохраняем путь к файлу в формате Uri
uri = Uri . fromFile (outputFile);
fileOutputStream . flush ();
fileOutputStream . close ();
} catch ( IOException e) {
e . printStackTrace ();
}
}
}
@ Override
public void onViewCreated (@ NonNull View view , @ Nullable Bundle savedInstanceState) {
super . onViewCreated (view , savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()) . get ( DataViewModel . class );
mContactImage = view . findViewById ( R . id . profile_image );
EditText nameEditText = view . findViewById ( R . id . name_et );
EditText EmailEditText = view . findViewById ( R . id . email_et );
EditText PhoneEditText = view . findViewById ( R . id . phone_et );
Button addContactButton = view . findViewById ( R . id . button_add );
addContactButton . setOnClickListener (v -> {
if (uri == null ) {
Toast . makeText ( requireContext() , "Contact image not set!" , Toast . LENGTH_SHORT ) . show ();
return ;
}
String name = nameEditText . getText () . toString ();
String email = EmailEditText . getText () . toString ();
String phone = PhoneEditText . getText () . toString ();
Contact contact = new Contact(name , email , phone , uri) ;
viewModel . addContact (contact);
getActivity() . onBackPressed ();
});
Button takePhotoButton = view . findViewById ( R . id . button_camera );
takePhotoButton . setOnClickListener (v -> {
Intent takePictureIntent = new Intent( MediaStore . ACTION_IMAGE_CAPTURE ) ;
try {
startActivityForResult(takePictureIntent , IMAGE_CAPTURE_REQUEST_CODE) ;
} catch ( ActivityNotFoundException e) {
Toast . makeText ( requireContext() , "Error while trying to open camera app" , Toast . LENGTH_SHORT ) . show ();
}
});
Button cancelButton = view . findViewById ( R . id . button_cancel );
cancelButton . setOnClickListener (v -> getActivity() . onBackPressed ());
}
}
Copy public class DataViewModel extends ViewModel {
private final MutableLiveData < List < Contact >> data = new MutableLiveData <>();
public DataViewModel () {
data . setValue ( new ArrayList <>());
}
public MutableLiveData < List < Contact >> getData () {
return data;
}
public void addContact ( Contact contact) {
List < Contact > list = data . getValue ();
list . add (contact);
data . setValue (list);
}
public void removeContact ( int index) {
List < Contact > list = data . getValue ();
list . remove (index);
data . setValue (list);
}
}
Copy public class ContactsAdapter extends RecyclerView . Adapter < ContactsAdapter . ContactHolder > {
public interface DeleteItemListener {
void onDeleteItem ( int position);
}
private final LayoutInflater inflater;
private final List < Contact > list;
private DeleteItemListener listener;
private static final int EMPTY_LIST_TYPE = 0 ;
private static final int NON_EMPTY_LIST_TYPE = 1 ;
public ContactsAdapter ( Context context , List < Contact > users , DeleteItemListener listener) {
this . inflater = LayoutInflater . from (context);
this . list = users;
this . listener = listener;
}
@ Override
public int getItemViewType ( int position) {
return list . isEmpty () ? EMPTY_LIST_TYPE : NON_EMPTY_LIST_TYPE;
}
@ NonNull
@ Override
public ContactHolder onCreateViewHolder (@ NonNull ViewGroup parent , int viewType) {
View view;
if (viewType == EMPTY_LIST_TYPE) {
view = inflater . inflate ( R . layout . list_no_items , parent , false );
view . setTag (EMPTY_LIST_TYPE);
} else {
view = inflater . inflate ( R . layout . list_contact , parent , false );
view . setTag (NON_EMPTY_LIST_TYPE);
}
return new ContactHolder(view , listener) ;
}
@ Override
public void onBindViewHolder (@ NonNull ContactHolder holder , int position) {
if ( getItemViewType(position) == EMPTY_LIST_TYPE)
return ;
Contact contact = list . get (position);
holder . image . setImageURI ( contact . getUri ());
holder . name . setText ( contact . getName ());
holder . email . setText ( contact . getEmail ());
holder . phone . setText ( contact . getPhone ());
}
@ Override
public int getItemCount () {
return Math . max ( list . size () , 1 );
}
static class ContactHolder extends RecyclerView . ViewHolder implements View . OnClickListener {
ImageView image;
TextView name;
TextView email;
TextView phone;
ImageButton deleteButton;
DeleteItemListener listener;
public ContactHolder (@ NonNull View itemView , DeleteItemListener listener) {
super(itemView);
image = itemView . findViewById ( R . id . contact_image );
name = itemView . findViewById ( R . id . name );
email = itemView . findViewById ( R . id . email );
phone = itemView . findViewById ( R . id . phone );
deleteButton = itemView . findViewById ( R . id . clearButton );
this . listener = listener;
if (( int ) itemView . getTag () == NON_EMPTY_LIST_TYPE) {
deleteButton . setOnClickListener ( this );
}
}
@ Override
public void onClick ( View v) {
listener . onDeleteItem ( getAdapterPosition() );
}
}
}
Copy public class Contact {
private String name;
private String email;
private String phone;
private Uri uri;
public Contact ( String name , String email , String phone , Uri uri) {
this . name = name;
this . email = email;
this . phone = phone;
this . uri = uri;
}
public String getName () {
return name;
}
public void setName ( String name) {
this . name = name;
}
public String getEmail () {
return email;
}
public void setEmail ( String email) {
this . email = email;
}
public String getPhone () {
return phone;
}
public void setPhone ( String phone) {
this . phone = phone;
}
public Uri getUri () {
return uri;
}
public void setUri ( Uri uri) {
this . uri = uri;
}
}