안드로이드에서 Firebase에 정보 저장하기 - 3(ViewModel, Activity)

2023. 5. 5. 17:12Mobile/Android

작성자알 수 없는 사용자

728x90
반응형

안녕하세요. 기깔나는 사람들에서 안드로이드를 맡고 있는 마플입니다.

이번에는 저번에 이어서  ViewModel Activity 살펴 볼게요.


ViewModel

1. InfoFormViewModel

1) base

public class InfoFormViewModel extends ViewModel {


    private final FirebaseStorageRepository mStorage;
    private final FirebaseStoreRepository mDB;
    /**
     * 정보 입력 창에 대한 검증 결과
     */
    private final MutableLiveData<InfoFormState> infoFormState;
    /**
     * 기기내의 사업증 URI
     */
    private final MutableLiveData<Uri> selectedFileUri;
    /**
     * 업로드 성공 여부
     */
    private final MutableLiveData<Boolean> uploadSuccess;

    InfoFormViewModel() {
        infoFormState = new MutableLiveData<>();
        selectedFileUri = new MutableLiveData<>();
        uploadSuccess = new MutableLiveData<>();
        // use emulator
        this.mStorage = FirebaseEmulatorStorage.getInstance();
        this.mDB = FirebaseEmulatorStore.getInstance();
    }

    
}

2) 거래처 정보 업로드

회사 이름, 대표 이름, 휴대 전화번호, 사업증을 보내는 데이터 포맷에 맞춰 wrapping 해준 뒤, Repository에 저장했습니다. 
성공 실패 여부를 uploadSuccess에 저장했어요.

/**
     * saves partner info to DB, after getting URL about uploaded a selected business license image.
     * 사진 파일을 업로드 후에 해당 URL와 입력된 partner 정보를 DB에 저장
     *
     * @param companyName - 회사 이름
     * @param CEOName     - 대표 이름
     * @param phoneNumber - 휴대 전화 번호
     */
    public void uploadInfoForm(String companyName, String CEOName, String phoneNumber) {
        mStorage.uploadBusinessLicenseFile(selectedFileUri.getValue(), new UploadFileListener() {
            @Override
            public void onSuccess(String url) {
                Map<String, Object> data = mDB.formatInfoForm(
                        GlobalApplication.getUser().getEmail(), companyName, CEOName, phoneNumber, url, false
                );

                mDB.saveCompaniesInfoForm(data, new SaveCompaniesInfoFormListener() {
                    @Override
                    public void onSuccess() {
                        uploadSuccess.postValue(true);
                    }

                    @Override
                    public void onError() {
                        uploadSuccess.postValue(false);
                    }
                });
            }

            @Override
            public void onError(Throwable throwable) {
                uploadSuccess.postValue(false);
            }
        });
    }

3) 데이터 검증

거래처 정보가 올바른 형식으로 입력이 됬는지 확인 합니다.

거래처 정보가 어디가 틀렸는지 InfoFormState에 에러 메시지 id값을 집어 넣습니다.

모두 통과 시에만 true값이 저장이 되요.

    /**
     * validates info form data(company name, CEO name, phone number, selected file)
     * info form에 들어가는 데이터 유효성 검사 (data = 회사 이름, 대표 이름, 휴대전화 번호, 선택한 파일)
     *
     * @param companyName - 회사 이름
     * @param CEOName     - 대표 이름
     * @param phoneNumber - 휴대전화 번호
     */
    public void infoFormChanged(String companyName, String CEOName, String phoneNumber) {
        if (!isCompanyNameValid(companyName)) {
            infoFormState.setValue(new InfoFormState(R.string.invalid_company_name, null, null, null));
        } else if (!isCEONameValid(CEOName)) {
            infoFormState.setValue(new InfoFormState(null, R.string.invalid_company_CEO, null, null));
        } else if (!RegexHelper.isPhoneNumberValid(phoneNumber)) {
            infoFormState.setValue(new InfoFormState(null, null, R.string.invalid_phone_number, null));
        } else if (selectedFileUri.getValue() == null) {
            infoFormState.setValue(new InfoFormState(null, null, null, R.string.not_found_file_wring));
        } else if (!isFileUriValid(selectedFileUri.getValue())) {
            infoFormState.setValue(new InfoFormState(null, null, null, R.string.invalid_file_uri));
        } else if (!isValidImage(selectedFileUri.getValue())) {
            infoFormState.setValue(new InfoFormState(null, null, null, R.string.invalid_image_extension));
        } else {
            infoFormState.setValue(new InfoFormState(true));
        }
    }

4) 문자열이 비어있는지 확인 

비어 있고 공백이 있는지까지 확인합니다.

    // 받은 문자열이 비어 있는지 확인 (공백 확인을 위해  trim 포함)
    private boolean isEmptyStr(String arg) {
        if (arg == null) {
            return false;
        } else return !arg.trim().equals("");
    }

5) 대표 이름 형식 확인

대표 이름은 문자열이 비어있는지만 확인합니다.


    // 대표 이름 확인을 위한 메소드 - 비어 있는 지만 확인
    private boolean isCEONameValid(String arg) {
        return isEmptyStr(arg);
    }

6) 사업증 URI 유요한지 확인

기기 내부에 있는 사업증 URI이 유효한지 확인합니다.

접근을 하여 파일이 있는지 확인하여 없으면 false 있으면 true를 반환합니다.

    // 유효한 file uri 인지 확인
    private boolean isFileUriValid(Uri uri) {
        ContentResolver contentResolver = GlobalApplication.getContext().getContentResolver();
        try (InputStream inputStream = contentResolver.openInputStream(uri)) {
            return inputStream != null;
        } catch (FileNotFoundException e) {
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

7) 이미지인지 확인

URI에 있는 파일이 확장자가 .jpg, .png인지 확인하여 맞으면 true를 반환 해줍니다.

    // check file extension for image - .jpg, png
    // 파일 확장자가 jpg나 png인지 확인
    private boolean isValidImage(Uri uri) {
        String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(GlobalApplication.getContext().getContentResolver().getType(uri));
        return extension != null && (extension.equals("jpg") || extension.equals("png"));
    }

8) 회사 이름 형식 확인

회사이름은 비었는지만 확인합니다.

    // 회사 이름 확인을 위한 메소드 - 비어 있는 지만 확인
    private boolean isCompanyNameValid(String arg) {
        return isEmptyStr(arg);
    }

Getter나 Setter은 생략하도록 하겠습니다.


Activity

1. InfoFormActivity

public class InfoFormActivity extends AppCompatActivity {

    private InfoFormViewModel mMainViewModel;
    private ActivityInfoFormBinding mMainBinding;
    /**
     * 거래처 정보를 담는 view binding
     */
    private ContentInfoFormBinding mIncludedBinding;
	...
}

1) onCreate()

view binding, viewmodel, view 요소를 가져와서 목적에 맞게 설정해줍니다. 

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // view binding
        mMainBinding = ActivityInfoFormBinding.inflate(getLayoutInflater());
        setContentView(mMainBinding.getRoot());
        mIncludedBinding = mMainBinding.infoFormIncludedInFormView;
        // init view model
        mMainViewModel = new ViewModelProvider(this).get(InfoFormViewModel.class);

        final EditText companyNameEditText = mIncludedBinding.infoFormCompanyNameEditText;
        final EditText companyCEONameEditText = mIncludedBinding.infoFormCompanyCEONameEditText;
        final EditText phoneEditText = mIncludedBinding.infoFormPhoneEditText;
        final ImageView uploadedFileImageView = mIncludedBinding.infoFormBusinessLicenseImageView;

        final Button businessLicenseSelectButton = mMainBinding.businessLicenseSelectButton;
        final Button saveButton = mMainBinding.saveButton;

		...
    }

이미지를 가져오기 위한 액션을 위해 객체를 생성 해줍니다.

객체 안에는 액션을 수행한 후에 URI를 추출하여 ViewModel에 전달하는 것이 있습니다.

        // 이미지를 가져오는 액션을 위한 launcher
        ActivityResultLauncher<Intent> launcher =
                this.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
                        result -> {
                            // 사진을 가져온 것이 성공 했을 시
                            if (result.getResultCode() == Activity.RESULT_OK) {
                                // uri 추출
                                assert result.getData() != null;
                                Uri uri = result.getData().getData();
                                // viewModel에 uri 저장
                                mMainViewModel.setSelectedFileUri(uri);
                            }
                        });

ViewModel안에 있는 InfoFormState를 보고 있어 해당 내용이 변경될 시 수행할 일을 넣습니다.

여기서는 InfoForm안에 있는 내용을 가지고 에러 메시지를 띄우거나 저장 버튼을 활성화 했습니다.

 // 거래처 입력 정보이 형식에 맞는지 확인한 infoFormState로 에러 내용 처리 및 결과에 따른 저장 버튼 활성화
        mMainViewModel.getInfoFormState().observe(this, infoFormState -> {
            if (infoFormState == null) {
                return;
            }
            // 저장 버튼을 데이터가 검증을 통과 했는지에 따라 설정
            saveButton.setEnabled(infoFormState.isDataValid());
            // 회사 이름의 형식에 존재 시
            if (infoFormState.getCompanyNameError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                companyNameEditText.setError(getString(infoFormState.getCompanyNameError()));
            }
            // 대표 이름의 형식에 문제가 있을 시
            if (infoFormState.getCEONameError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                companyCEONameEditText.setError(getString(infoFormState.getCEONameError()));
            }
            // 전화 번호 형식에 문제가 있을 시
            if (infoFormState.getPhoneNumberError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                phoneEditText.setError(getString(infoFormState.getPhoneNumberError()));
            }
            // 선택한 사업증 데이터에 문제가 있을 시
            if (infoFormState.getBusinessLicenseError() != null) {
                // 에러 내용을 선택 버튼에 툴팁으로 띄우기
                businessLicenseSelectButton.setError(getString(infoFormState.getBusinessLicenseError()));
            }
        });

갤러리에서 가져온 사업증은 ViewModel에 저장하기 때문에, 저장된 URI이 있는 SelectedFileUri를 감지하여 해당 URI를 가지고 화면 ImageView에 띄워 줍니다.

Gilde를 이용하여 최대 크기를 지정하여 이미지가 너무 크면 화면이 깨지는 것을 방지 했습니다.

        // 갤러리에서 가져온 데이터를 화면에서 보여줌
        mMainViewModel.getSelectedFileUri().observe(this, uri -> {
            // not null
            if (uri == null)
                return;
            // 화면의 사업증 띄워주기 - 최대 크기(화면 너비, 300)를 설정 하여 이미지 로드
            Glide.with(this)
                    .load(uri)
                    .override(uploadedFileImageView.getWidth(), 300)
                    .into(uploadedFileImageView);

            // 입력 데이터 검증
            mMainViewModel.infoFormChanged(
                    companyNameEditText.getText().toString(),
                    companyCEONameEditText.getText().toString(),
                    phoneEditText.getText().toString()
            );
        });

업로드 결과를 ViewModel에서 가져와서 성공 시 메시지와 함께 Activity를 종료해줍니다.
실패시에는 실패 메시지를 띄워줍니다.

        /*  업로드 결과
            성공 시 성공 메시지 띄우고 1초 뒤 종료,
            실패 시 실패 메시지 띄움
         */
        mMainViewModel.isUploadSuccess().observe(this, aBoolean -> {
            if (aBoolean) {
                showUploadSuccess();
                exitActivity();
            } else {
                showUploadFail();
            }
        });

거래처 정보를 입력하는 view에서 입력 된 후에 감지하여 거래처 정보 형식이 맞는지 검증하도록 합니다.

        // 회사 이름, 대표 이름, 전화번호 editText 감지를 위한 textWatch 선언
        TextWatcher afterTextChangedListener = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                // ignore
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                // ignore
            }

            @Override
            public void afterTextChanged(Editable editable) {
                // 변경 감지 후 viewModel에 데이터 저장
                mMainViewModel.infoFormChanged(
                        companyNameEditText.getText().toString(),
                        companyCEONameEditText.getText().toString(),
                        phoneEditText.getText().toString()
                );
            }
        };
        // 거래처 정보 입력창을 감시하는 감시자 설정
        companyNameEditText.addTextChangedListener(afterTextChangedListener);
        companyCEONameEditText.addTextChangedListener(afterTextChangedListener);
        phoneEditText.addTextChangedListener(afterTextChangedListener);

버튼을 누르면 이미지를 고를 수 있도록 리스너를 등록합니다.

        // 갤러리에서 파일을 고를 수 있도록 새로운 액티비티 실행
        businessLicenseSelectButton.setOnClickListener(view -> {
            Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            intent.setType("image/*");
            launcher.launch(intent);
        });

저장 버튼을 누르면 입력창에 있는 데이터를 viewModel에 전달하여 업로드 절차가 될 수 있도록 호출합니다.

        // 저장 버튼을 누를 시 업로드 진행
        saveButton.setOnClickListener((view) -> {
            String companyName = companyNameEditText.getText().toString();
            String companyCEOName = companyCEONameEditText.getText().toString();
            String phoneNumber = phoneEditText.getText().toString();
            mMainViewModel.uploadInfoForm(companyName, companyCEOName, phoneNumber);
        });

이미지가 작아서 안 보일 수도 있어서 화면에 가득차게 보이도록, 이미지를 클릭하면 이미지를 하나의 화면에 띄워줍니다.

        // 선택한 파일 크게 보는 액티비티로 이동
        uploadedFileImageView.setOnClickListener((view) -> {
            if (mMainViewModel.getSelectedFileUri().getValue() != null) {
                Intent intent = new Intent(InfoFormActivity.this, ImageDetailActivity.class);
                intent.putExtra("imageUri", mMainViewModel.getSelectedFileUri().getValue().toString()); // 이미지 Uri를 전달
                startActivity(intent);
            } else {
                showUnselectedFileWring();
            }
        });​

2) 메시지 및 액티비티 종료

각 상황별 띄워줄 메시지 메소드와 특정 조건에서 액티비티 종료할 때 해야 할 일을 넣은 메소드입니다.

    /**
     * 파일이 선택을 하지 않는 상태로 이미지 뷰를 클릭 시 경고 메시지 띄워 주기
     */
    private void showUnselectedFileWring() {
        // 경고 메시지
        String wring = getString(R.string.not_found_file_wring);
        Toast.makeText(getApplicationContext(), wring, Toast.LENGTH_LONG).show();
    }

    /**
     * 업로드 성공에 대한 메시지 띄워 주기
     */
    private void showUploadSuccess() {
        String message = "업로드에 성공했습니다.";
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }

    //

    /**
     * 업로드 실패에 대한 메시지 띄워 주기
     */
    private void showUploadFail() {
        String message = "업로드에 실패했습니다.";
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }
    /**
     * 1초 지연 후 액티비티 종료
     */
    private void exitActivity() {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            // 액티비티 종료
            finish();
        }, 1000);
    }

Activity 전체 코드입니다.

public class InfoFormActivity extends AppCompatActivity {

    private InfoFormViewModel mMainViewModel;
    private ActivityInfoFormBinding mMainBinding;
    /**
     * 거래처 정보를 담는 view binding
     */
    private ContentInfoFormBinding mIncludedBinding;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // view binding
        mMainBinding = ActivityInfoFormBinding.inflate(getLayoutInflater());
        setContentView(mMainBinding.getRoot());
        mIncludedBinding = mMainBinding.infoFormIncludedInFormView;
        // init view model
        mMainViewModel = new ViewModelProvider(this).get(InfoFormViewModel.class);

        final EditText companyNameEditText = mIncludedBinding.infoFormCompanyNameEditText;
        final EditText companyCEONameEditText = mIncludedBinding.infoFormCompanyCEONameEditText;
        final EditText phoneEditText = mIncludedBinding.infoFormPhoneEditText;
        final ImageView uploadedFileImageView = mIncludedBinding.infoFormBusinessLicenseImageView;

        final Button businessLicenseSelectButton = mMainBinding.businessLicenseSelectButton;
        final Button saveButton = mMainBinding.saveButton;


        // 이미지를 가져오는 액션을 위한 launcher
        ActivityResultLauncher<Intent> launcher =
                this.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
                        result -> {
                            // 사진을 가져온 것이 성공 했을 시
                            if (result.getResultCode() == Activity.RESULT_OK) {
                                // uri 추출
                                assert result.getData() != null;
                                Uri uri = result.getData().getData();
                                // viewModel에 uri 저장
                                mMainViewModel.setSelectedFileUri(uri);
                            }
                        });
        // 거래처 입력 정보이 형식에 맞는지 확인한 infoFormState로 에러 내용 처리 및 결과에 따른 저장 버튼 활성화
        mMainViewModel.getInfoFormState().observe(this, infoFormState -> {
            if (infoFormState == null) {
                return;
            }
            // 저장 버튼을 데이터가 검증을 통과 했는지에 따라 설정
            saveButton.setEnabled(infoFormState.isDataValid());
            // 회사 이름의 형식에 존재 시
            if (infoFormState.getCompanyNameError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                companyNameEditText.setError(getString(infoFormState.getCompanyNameError()));
            }
            // 대표 이름의 형식에 문제가 있을 시
            if (infoFormState.getCEONameError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                companyCEONameEditText.setError(getString(infoFormState.getCEONameError()));
            }
            // 전화 번호 형식에 문제가 있을 시
            if (infoFormState.getPhoneNumberError() != null) {
                // 에러 내용을 툴팁으로 띄우기
                phoneEditText.setError(getString(infoFormState.getPhoneNumberError()));
            }
            // 선택한 사업증 데이터에 문제가 있을 시
            if (infoFormState.getBusinessLicenseError() != null) {
                // 에러 내용을 선택 버튼에 툴팁으로 띄우기
                businessLicenseSelectButton.setError(getString(infoFormState.getBusinessLicenseError()));
            }
        });

        // 갤러리에서 가져온 데이터를 화면에서 보여줌
        mMainViewModel.getSelectedFileUri().observe(this, uri -> {
            // not null
            if (uri == null)
                return;
            // 화면의 사업증 띄워주기 - 최대 크기(화면 너비, 300)를 설정 하여 이미지 로드
            Glide.with(this)
                    .load(uri)
                    .override(uploadedFileImageView.getWidth(), 300)
                    .into(uploadedFileImageView);

            // 입력 데이터 검증
            mMainViewModel.infoFormChanged(
                    companyNameEditText.getText().toString(),
                    companyCEONameEditText.getText().toString(),
                    phoneEditText.getText().toString()
            );
        });
        /*  업로드 결과
            성공 시 성공 메시지 띄우고 1초 뒤 종료,
            실패 시 실패 메시지 띄움
         */
        mMainViewModel.isUploadSuccess().observe(this, aBoolean -> {
            if (aBoolean) {
                showUploadSuccess();
                exitActivity();
            } else {
                showUploadFail();
            }
        });

        // 회사 이름, 대표 이름, 전화번호 editText 감지를 위한 textWatch 선언
        TextWatcher afterTextChangedListener = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                // ignore
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                // ignore
            }

            @Override
            public void afterTextChanged(Editable editable) {
                // 변경 감지 후 viewModel에 데이터 저장
                mMainViewModel.infoFormChanged(
                        companyNameEditText.getText().toString(),
                        companyCEONameEditText.getText().toString(),
                        phoneEditText.getText().toString()
                );
            }
        };
        // 거래처 정보 입력창을 감시하는 감시자 설정
        companyNameEditText.addTextChangedListener(afterTextChangedListener);
        companyCEONameEditText.addTextChangedListener(afterTextChangedListener);
        phoneEditText.addTextChangedListener(afterTextChangedListener);

        // 갤러리에서 파일을 고를 수 있도록 새로운 액티비티 실행
        businessLicenseSelectButton.setOnClickListener(view -> {
            Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            intent.setType("image/*");
            launcher.launch(intent);
        });

        // 선택한 파일 크게 보는 액티비티로 이동
        uploadedFileImageView.setOnClickListener((view) -> {
            if (mMainViewModel.getSelectedFileUri().getValue() != null) {
                Intent intent = new Intent(InfoFormActivity.this, ImageDetailActivity.class);
                intent.putExtra("imageUri", mMainViewModel.getSelectedFileUri().getValue().toString()); // 이미지 Uri를 전달
                startActivity(intent);
            } else {
                showUnselectedFileWring();
            }
        });

        // 저장 버튼을 누를 시 업로드 진행
        saveButton.setOnClickListener((view) -> {
            String companyName = companyNameEditText.getText().toString();
            String companyCEOName = companyCEONameEditText.getText().toString();
            String phoneNumber = phoneEditText.getText().toString();
            mMainViewModel.uploadInfoForm(companyName, companyCEOName, phoneNumber);
        });
    }

    /**
     * 파일이 선택을 하지 않는 상태로 이미지 뷰를 클릭 시 경고 메시지 띄워 주기
     */
    private void showUnselectedFileWring() {
        // 경고 메시지
        String wring = getString(R.string.not_found_file_wring);
        Toast.makeText(getApplicationContext(), wring, Toast.LENGTH_LONG).show();
    }

    /**
     * 업로드 성공에 대한 메시지 띄워 주기
     */
    private void showUploadSuccess() {
        String message = "업로드에 성공했습니다.";
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }

    //

    /**
     * 업로드 실패에 대한 메시지 띄워 주기
     */
    private void showUploadFail() {
        String message = "업로드에 실패했습니다.";
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }
    /**
     * 1초 지연 후 액티비티 종료
     */
    private void exitActivity() {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            // 액티비티 종료
            finish();
        }, 1000);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mMainBinding = null;
        mIncludedBinding = null;
        mMainViewModel = null;
    }
}

이상으로 Firebase에 데이터를 저장하는 것을 해보았습니다. 
궁금한점이 있으면 댓글을 달아 주세요!

728x90
반응형