Как вернуть значение DataSnapshot в результате метода?

У меня нет большого опыта работы с Java. Я не уверен, что этот вопрос глуп, но мне нужно получить имя пользователя из базы данных Firebase в реальном времени и вернуть это имя в результате этого метода. Итак, я выяснил, как получить это значение, но я не понимаю, как вернуть его как результат этого метода. Какой лучший способ сделать это?

private String getUserName(String uid) {
    databaseReference.child(String.format("users/%s/name", uid))
            .addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            // How to return this value?
            dataSnapshot.getValue(String.class);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {}
    });
}

Ответ 1

Это классическая проблема с асинхронными веб-API. Вы не можете вернуть то, что еще не было загружено. Другими словами, вы не можете просто создать глобальную переменную и использовать ее вне onDataChange() потому что она всегда будет null. Это происходит потому, что onDataChange() называется асинхронным. В зависимости от скорости вашего соединения и состояния, эти данные могут занять от нескольких сотен миллисекунд до нескольких секунд.

Но не только Firebase Realtime Database загружает данные асинхронно, почти все другие современные веб-API делают, так как это может занять некоторое время. Таким образом, вместо ожидания данных (что может привести к не отвечающим диалогам приложений для ваших пользователей), ваш основной код приложения продолжается, пока данные загружаются во вторичный поток. Затем, когда данные доступны, вызывается ваш метод onDataChange(), который может использовать данные. Другими словами, к моменту onDataChange() метода onDataChange() ваши данные еще не загружены.

Давайте возьмем пример, разместив несколько операторов журнала в коде, чтобы более четко увидеть, что происходит.

private String getUserName(String uid) {
    Log.d("TAG", "Before attaching the listener!");
    databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            // How to return this value?
            dataSnapshot.getValue(String.class);
            Log.d("TAG", "Inside onDataChange() method!");
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {}
    });
    Log.d("TAG", "After attaching the listener!");
}

Если мы запустим этот код, результат будет:

Прежде чем прикрепить слушателя!

После прикрепления слушателя!

Внутри метода onDataChange()!

Вероятно, это не то, что вы ожидали, но это объясняет, почему ваши данные null при их возврате.

Первоначальный ответ для большинства разработчиков состоит в том, чтобы попытаться "исправить" это asynchronous behavior, которое я лично рекомендую против этого. Сеть асинхронна, и чем раньше вы это примете, тем быстрее вы научитесь работать с современными веб-API.

Я нашел, что легче всего переосмыслить проблемы для этой асинхронной парадигмы. Вместо того, чтобы сказать "Сначала получите данные, затем зарегистрируйте их", я обозначаю проблему как "Начать получать данные. Когда данные загружены, зарегистрируйте их". Это означает, что любой код, которому требуются данные, должен быть внутри onDataChange() или вызываться изнутри, например:

databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // How to return this value?
        if(dataSnapshot != null) {
            System.out.println(dataSnapshot.getValue(String.class));
        }
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {}
});

Если вы хотите использовать это снаружи, есть другой подход. Вам нужно создать свой собственный обратный вызов, чтобы дождаться, пока Firebase вернет вам данные. Для этого сначала нужно создать такой interface:

public interface MyCallback {
    void onCallback(String value);
}

Затем вам нужно создать метод, который на самом деле получает данные из базы данных. Этот метод должен выглядеть так:

public void readData(MyCallback myCallback) {
    databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            String value = dataSnapshot.getValue(String.class);
            myCallback.onCallback(value);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {}
    });
}

В конце просто вызовите readData() и передайте экземпляр интерфейса MyCallback в качестве аргумента везде, где он вам нужен, например:

readData(new MyCallback() {
    @Override
    public void onCallback(String value) {
        Log.d("TAG", value);
    }
});

Это единственный способ использовать это значение вне onDataChange(). Для получения дополнительной информации вы также можете посмотреть это видео.

Ответ 2

Здесь сумасшедшая идея, внутри onDataChange, поместить ее в TextView с textview.setVisiblity(Gone) видимостью textview.setVisiblity(Gone) или что-то в этом роде, а затем сделать что-то вроде

textview.setText(dataSnapshot.getValue(String.class))

затем позже получите это с textview.getText().toString()

просто сумасшедшая простая идея.

Ответ 3

Полагаю, я понимаю, о чем вы спрашиваете. Хотя вы говорите, что хотите "вернуть" его (по сути) из метода выборки, достаточно сказать, что вы просто хотите использовать значение, полученное после завершения выборки. Если это так, это то, что вам нужно сделать:

  • Создайте переменную в верхней части вашего класса
  • Извлеките свое значение (которое вы сделали в основном правильно)
  • Установить общедоступную переменную в вашем классе, равную полученному значению.

Как только ваша выборка будет успешной, вы можете сделать много вещей с переменной. 4а и 4b - несколько простых примеров:

4а. Edit: В качестве примера использования вы можете запускать все, что вам нужно для запуска в своем классе, который использует yourNameVariable (и вы можете быть уверены, что он yourNameVariable не равен нулю)

4b. Edit: В качестве примера использования вы можете использовать переменную в функции, которая запускается кнопкой onClickListener.


Попробуйте это.

// 1. Create a variable at the top of your class
private String yourNameVariable;

// 2. Retrieve your value (which you have done mostly correctly)
private void getUserName(String uid) {
    databaseReference.child(String.format("users/%s/name", uid))
            .addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            // 3. Set the public variable in your class equal to value retrieved
            yourNameVariable = dataSnapshot.getValue(String.class);
            // 4a. EDIT: now that your fetch succeeded, you can trigger whatever else you need to run in your class that uses `yourNameVariable`, and you can be sure `yourNameVariable` is not null.
            sayHiToMe();
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {}
    });
}

// (part of step 4a)
public void sayHiToMe() {
  Log.d(TAG, "hi there, " + yourNameVariable);
}

// 4b. use the variable in a function triggered by the onClickListener of a button.
public void helloButtonWasPressed() {
  if (yourNameVariable != null) {
    Log.d(TAG, "hi there, " + yourNameVariable);
  }
}

Затем вы можете использовать yourNameVariable везде, где бы вы хотели, во всем классе.


Примечание. Просто убедитесь, что вы проверяете, что yourNameVariable не является нулевым при его использовании, поскольку onDataChange является асинхронным и может не выполняться в то время, когда вы пытаетесь использовать его в другом месте.

Ответ 4

Это работает, вы можете использовать значения из datasnapshot вне onDataChange, передав значение в качестве параметра другой функции, см. Мой ответ ниже, он работает, и я использую его повсеместно в моем приложении.

// Get Your Value
private void getValue() {

    fbDbRefRoot.child("fbValue").addListenerForSingleValueEvent(new ValueEventListener() {

        @Override
        public void onDataChange(@NonNull DataSnapshot dataSnapshot) {

            String yourValue = (String) dataSnapshot.getValue();
            useValue(yourValue);

        }

        @Override
        public void onCancelled(@NonNull DatabaseError databaseError) {

        }
    });

}

// Use Your Value
private void useValue(String yourValue) {

    Log.d(TAG, "countryNameCode: " + yourValue);

}

Основное различие между моим ответом и ответом Rbar заключается в том, что я передаю значение в качестве параметра в функцию, а затем использую его.