In a previous post I've shown you how to use the HQUIC kit to perform a simple GET request to download the latest local news by using a third party API. At this point everything is ok, but, what if I want to send a request with headers? or, how can I perform a POST request?. If you have made the same questions, please keep reading.
Previous requirements
An Android Studio project
Integrating the HQUIC SDK
HQUC will perform HTTP requests over the QUIC protocol to let your users enjoy faster connections with lower bandwidth. If the remote server does not support QUIC, the kit will use HTTP V2 instead, so, you just need to code once.
To add the HQUIC kit to your app, add the next dependency to your app level build.gradle file
Code:
implementation 'com.huawei.hms:hquic-provider:5.0.0.300'
Sync your project and you will be ready to use the HQUIC SDK. We will reuse the HQUICService class provided on the HQUIC sample code, but with little modifications
Code:
class HQUICService(val context: Context) {
private val TAG = "HQUICService"
private val DEFAULT_PORT = 443
private val DEFAULT_ALTERNATEPORT = 443
private val executor: Executor = Executors.newSingleThreadExecutor()
private var cronetEngine: CronetEngine? = null
private var callback: UrlRequest.Callback? = null
/**
* Asynchronous initialization.
*/
init {
HQUICManager.asyncInit(
context,
object : HQUICManager.HQUICInitCallback {
override fun onSuccess() {
Log.i(TAG, "HQUICManager asyncInit success")
}
override fun onFail(e: Exception?) {
Log.w(TAG, "HQUICManager asyncInit fail")
}
})
}
/**
* Create a Cronet engine.
*
* @param url URL.
* @return cronetEngine Cronet engine.
*/
private fun createCronetEngine(url: String): CronetEngine? {
if (cronetEngine != null) {
return cronetEngine
}
val builder = CronetEngine.Builder(context)
builder.enableQuic(true)
builder.addQuicHint(getHost(url), DEFAULT_PORT, DEFAULT_ALTERNATEPORT)
cronetEngine = builder.build()
return cronetEngine
}
/**
* Construct a request
*
* @param url Request URL.
* @param method method Method type.
* @return UrlRequest urlrequest instance.
*/
private fun builRequest(
url: String,
method: String,
headers: HashMap<String, String>?,
body:ByteArray?
): UrlRequest? {
val cronetEngine: CronetEngine? = createCronetEngine(url)
val requestBuilder = cronetEngine?.newUrlRequestBuilder(url, callback, executor)
requestBuilder?.apply {
setHttpMethod(method)
if(method=="POST"){
body?.let {
setUploadDataProvider(UploadDataProviders.create(ByteBuffer.wrap(it)), executor) }
}
headers?.let{
for (key in it.keys) {
addHeader(key, headers[key])
}
}
return build()
}
return null
}
/**
* Send a request to the URL.
*
* @param url Request URL.
* @param method Request method type.
*/
fun sendRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null) {
Log.i(TAG, "callURL: url is " + url + "and method is " + method)
val urlRequest: UrlRequest? = builRequest(url, method, headers,body)
urlRequest?.apply { urlRequest.start() }
}
/**
* Parse the domain name to obtain the host name.
*
* @param url Request URL.
* @return host Host name.
*/
private fun getHost(url: String): String? {
var host: String? = null
try {
val url1 = URL(url)
host = url1.host
} catch (e: MalformedURLException) {
Log.e(TAG, "getHost: ", e)
}
return host
}
fun setCallback(mCallback: UrlRequest.Callback?) {
callback = mCallback
}
}
The sendRequest method has been modified to receive a HashMap with the headers, and a ByteArray with the Body payload. Note the sendRequest method if the body or the headers are not null, will be added to the request.
Code:
requestBuilder?.apply {
setHttpMethod(method)
if(method=="POST"){
body?.let {//Adding the request Body
setUploadDataProvider(UploadDataProviders.create(ByteBuffer.wrap(it)), executor) }
}
headers?.let{
for (key in it.keys) {//Adding all the headers
addHeader(key, headers[key])
}
}
With that modifications can perform an HTTP request by this way
Code:
val map=HashMap<String,String>()
map["Content-Type"] = "application/json"
val body=JSONObject().apply {
put("key1","value1")
put("key2","value2")
}
HQUICService(context).sendRequest(HOST,"POST",map,body.toString().toByteArray())
That's enough to send a request, but, what about the response? HQUIC provides an Abstract Class for listening the request events. All we need is inherit from UrlRequest.Callback. Let's do it.
Code:
class HQUICClient(context: Context) : UrlRequest.Callback() {
var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="QUICClient"
val response=ByteArrayOutputStream()
var listener:QuicClientListener?=null
init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}
fun makeRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null){
hquicService?.sendRequest(url,method,headers,body)
}
override fun onRedirectReceived(
request: UrlRequest?,
info: UrlResponseInfo?,
newLocationUrl: String?
) {
request?.followRedirect()
}
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request?.read(byteBuffer)
}
override fun onReadCompleted(
request: UrlRequest?,
info: UrlResponseInfo?,
byteBuffer: ByteBuffer?
) {
byteBuffer?.apply {
response.write(array(),arrayOffset(),position())
response.flush()
}
request?.read(ByteBuffer.allocateDirect(CAPACITY))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
listener?.onSuccess(response.toByteArray())
}
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
listener?.apply { onFailure(error.toString()) }
}
Remember, certain number of bytes can be readed per time, so for long responses, the method onReadCompleated will be called multiple times until the response has been successfully readed, or an error ocurs. When the operation is complete, the onSucceeded callback will be called and you will be able to parse the response. If the request fails, you will get an exception on the onFailed callback.
To report the request result, you can create a public interface
Code:
interface HQUICClientListener{
fun onSuccess(response: ByteArray)
fun onFailure(error: String)
}
And then, if the response is succesful, you can parse your byte array properly.
Code:
override fun onSuccess(response: ByteArray) {
//For text
Log.i(TAG, String(response))
//For images
BitmapFactory.decodeByteArray(response,0,response.size)
}
Conclusion
With HQUIC you can easily create a REST client for your android app, taking advantage of the QUIC features and keeping HTTP 2 compatibility.
Reference
HQUIC developer guide
Related
Introduction
This article is based on Huawei Mobile Services application. I have developed Trip Booking Android app. We can provide the solution for HMS based multiple kits such as Account Kit, Huawei Ads, Huawei Map, and Huawei Analysts to use in Trip Booking. So users can book any trip.
In this application, users can plan trips and book their trips. It will provide the ongoing trip cities wise with weather forecasting so that user can easily plan a trip.
In this article, I will integrate Weather API, Huawei Map, and Huawei Map Direction API, so that users can check the route and plan their trips, and book with the trip weather forecast.
Huawei Map
HMS Core Map SDK is a set of APIs for map development in Android. The map data covers most countries outside China and supports multiple languages. The Map SDK uses the WGS 84 GPS coordinate system, which can meet most requirements of map development outside China. You can easily add map-related functions in your Android app, including:
1. Map display: Displays buildings, roads, water systems, and Points of Interest (POIs).
2. Map interaction: Controls the interaction gestures and buttons on the map.
3. Map drawing: Adds location markers, map layers, overlays, and various shapes.
Prerequisite
1. A computer (desktop or laptop)
2. A Huawei phone, which is used to debug the developed app
3. HUAWEI Analytics Kit 5.0.3
4. Android SDK applicable to devices using Android API-Level 19 (Android 4.4 KitKat) or higher
5. Android Studio
6. Java JDK 1.7 or later (JDK 1.8 recommended).
Things Need To Be Done
To integrate HUAWEI HMS Core services, you must complete the following preparations:
1. Create an app in AppGallery Connect.
2. Create an Android Studio project.
3. Add the app package name and save the configuration file.
4. Configure the Maven repository address and AppGallery Connect gradle plug-in.
Integration
1. Sign in to AppGallery Connect and select my projects.
2. Navigate to app to enable Map Kit.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
3. Navigate to project setting and download the configuration file.
4. Add the Maven repository address to repositories.
Code:
buildscript {
repositories {
maven { url 'https://developer.huawei.com/repo/' }
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath 'com.huawei.agconnect:agcp:1.2.0.300'
}
}
allprojects {
repositories {
maven { url 'https://developer.huawei.com/repo/' }
google()
jcenter()
}
}
5. Add the AppGallery Connect dependency to dependencies.
Code:
//map
implementation 'com.huawei.hms:maps:4.0.0.301'
6. I have created the following class in which I have implemented Map Kit.
Code:
public class PolylineActivity extends AppCompatActivity implements OnMapReadyCallback {
public static final String TAG = "PolylineActivity";
private static final String MAPVIEW_BUNDLE_KEY = "MapViewBundleKey";
private HuaweiMap hmap;
private MapView mMapView;
private Marker mMarker;
private List<LatLng> latLngList;
private MapApiViewModel mapApiViewModel;
@Override
protected void onStart() {
super.onStart();
mMapView.onStart();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polyline);
mapApiViewModel = ViewModelProviders.of(this).get(MapApiViewModel.class);
mMapView = findViewById(R.id.mapview_mapviewdemo);
Bundle mapViewBundle = null;
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAPVIEW_BUNDLE_KEY);
}
mMapView.onCreate(mapViewBundle);
mMapView.getMapAsync(PolylineActivity.this);
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mMapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
mMapView.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mMapView.onDestroy();
}
@Override
public void onMapReady(HuaweiMap map) {
hmap = map;
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
hmap.setMyLocationEnabled(false);
hmap.setTrafficEnabled(true);
hmap.getUiSettings().setRotateGesturesEnabled(true);
hmap.getUiSettings().setCompassEnabled(false);
hmap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLngList.get(0), 12.0f));
hmap.addMarker(new MarkerOptions().position(latLngList.get(0)));
mapApiViewModel.getPolylineLiveData(getPolylineBody()).observe(this, result -> {
Log.d(TAG, result.toString());
getPolylineData(result);
});
}
Huawei Map Direction API
Huawei Map provides Direction API, so that user can access all the information related to Map in RESTful API.
Huawei has provide the following API endpoint to access Direction API.
https://mapapi.cloud.huawei.com/mapApi/v1
Huawei provide the following direction API:
1. Walking Route Planning
2. Bicycling Route Planning
3. Driving Route Planning
I have implemented the Driving Route API with the help of Retrofit and MVVM.
Retrofit Client
I have created MapApiClient class for accessing the Direction API.
Code:
public class MapApiClient {
private final static HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
private static OkHttpClient okHttpClient;
public static Service getClient() {
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Consants.BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
return retrofit.create(Service.class);
}
public interface Service {
@POST("mapApi/v1/routeService/driving")
Single<PolylineResponse> getPolylines(
@Query("key") String apiKey,
@Body PolylineBody polylineBody);
}
}
API Repository
I have created MapApiRepo class for accessing the API client.
Code:
public class MapApiRepo {
private MapApiClient.Service mService;
public MapApiRepo() {
this.mService = MapApiClient.getClient();
}
public Single<PolylineResponse> executeMapApi(PolylineBody polylineBody) {
return mService.getPolylines(Consants.API_KEY, polylineBody);
}
}
ViewModel
I have created MapApiViewModel class for handling the API calls.
Code:
public class MapApiViewModel extends ViewModel {
private final CompositeDisposable disposables = new CompositeDisposable();
private MapApiRepo mapApiRepo = new MapApiRepo();
private MutableLiveData<PolylineResponse> mPolylineLiveData = new MutableLiveData<>();
public LiveData<PolylineResponse> getPolylineLiveData(PolylineBody body) {
disposables.add(mapApiRepo.executeMapApi(body)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> mPolylineLiveData.setValue(result),
throwable -> mPolylineLiveData.setValue(null)
));
return mPolylineLiveData;
}
@Override
protected void onCleared() {
disposables.clear();
}
}
Drawing Polyline
I have implemented this functionality in the following activity.
Code:
mapApiViewModel.getPolylineLiveData(getPolylineBody()).observe(this, result -> {
Log.d(TAG, result.toString());
getPolylineData(result);
}); private PolylineBody getPolylineBody() {
PolylineBody polylineBody = new PolylineBody();
Origin origin = new Origin();
origin.setLat("30.0444");
origin.setLng("31.2357");
Destination destination = new Destination();
destination.setLat("30.0131");
destination.setLng("31.2089");
polylineBody.setDestination(destination);
polylineBody.setOrigin(origin);
return polylineBody;
}
public void getPolylineData(PolylineResponse polylineResponse) {
List<Routes> routesList = polylineResponse.getRoutes();
List<Paths> paths = new ArrayList<>();
List<Steps> steps = new ArrayList<>();
List<Polyline> polylines = new ArrayList<>();
latLngList = new ArrayList<>();
for (int x = 0; x < routesList.size(); x++) {
//here we can access each array list with main.get(x).
for (Paths paths1 : routesList.get(x).getPaths()) {
paths.add(paths1);
}
for (int y = 0; y < paths.size(); y++) {
for (Steps step :
paths.get(y).getSteps()) {
steps.add(step);
}
}
for (int i = 0; i < steps.size(); i++) {
for (Polyline polyline :
steps.get(i).getPolyline()) {
polylines.add(polyline);
}
}
}
for (int i = 0; i < polylines.size(); i++) {
latLngList.add(new LatLng(Double.valueOf(polylines.get(i).getLat())
, Double.valueOf(polylines.get(i).getLng())));
}
hmap.addPolyline(new PolylineOptions()
.addAll(latLngList)
.color(Color.BLUE)
.width(3));
}
Weather API
I have used weatherstack api to get city weather condition.
https://api.weatherstack.com/
WeatherRetrofit Client
I have implemented Weather API using retrofit library with RxJava2.
Code:
public class Client {
private final static HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
private static OkHttpClient okHttpClient;
public static Service getClient() {
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Utils.BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
return retrofit.create(Service.class);
}
public interface Service {
@GET("current")
Single<CurrentWeather> getCurrentWeather(
@Query("access_key") String apiKey,
@Query("query") String cityName);
@GET("forecast")
Single<ForcastWeather> getForecastWeather(
@Query("access_key") String apiKey,
@Query("query") String cityName,
@Query("forecast_days") String days);
}
}
App Development
I have created the following package inside the project. In which I have integrated Huawei Id Login, Huawei Analytics, Huawei Banner Ads, Weather API, Huawei Map, and Huawei Direction APIs.
LoginActivity
In this screen, I have integrated login functionality with Huawei Id along with Analytics Kit which logs the event.
Code:
if (authHuaweiIdTask.isSuccessful()) {
AuthHuaweiId huaweiAccount = authHuaweiIdTask.getResult();
Log.i(TAG, huaweiAccount.getDisplayName() + " signIn success ");
Log.i(TAG, "AccessToken: " + huaweiAccount.getAccessToken());
Bundle bundle = new Bundle();
bundle.putString(TAG,huaweiAccount.getDisplayName() + " signIn success ");
Analystics.getInstance(this).setEvent("login",bundle);
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("user", huaweiAccount.getDisplayName());
startActivity(intent);
this.finish();
}
HomeFragment
In this screen, I have implemented Huawei Ads and Analytics kit.
Which helps to log the user activity and shown banner ads.
Loading Banner Ads
Code:
private void initAds(View view) {
HwAds.init(getActivity());
hwBannerView = view.findViewById(R.id.huawei_banner_view);
hwBannerView.setVisibility(View.VISIBLE);
AdParam adParam = new AdParam.Builder().build();
hwBannerView.loadAd(adParam);
hwBannerView.setAdListener(adListener);
}
Log User Events
Code:
Bundle bundle = new Bundle();
bundle.putString(TAG,"City loaded");
Analystics.getInstance(getActivity()).setEvent("City",bundle);
cityList.setLayoutManager(new GridLayoutManager(getActivity(), 2));
cityList.setAdapter(new CityAdapter(cities, (item) -> {
Bundle bundle1 = new Bundle();
bundle.putString(TAG,"City Clicked"+item.getCityName());
Analystics.getInstance(getActivity()).setEvent("City",bundle1);
PopularCity popularCity = item;
Intent intent = new Intent(getActivity(), CityInfoDetailActivity.class);
intent.putExtra("name", popularCity.getCityName());
intent.putExtra("url", popularCity.getImageUrl());
startActivity(intent);
}));
CityInfoDetailActivity
In this screen, I have implemented the Huawei Banner ads and Huawei Analytics.
Loading Banner Ads
Code:
HwAds.init(this);
hwBannerView = findViewById(R.id.huawei_banner_view);
hwBannerView.setVisibility(View.VISIBLE);
AdParam adParam = new AdParam.Builder().build();
hwBannerView.loadAd(adParam);
hwBannerView.setAdListener(adListener);
Log User Events
Code:
if (extras != null) {
String name = extras.getString("name");
String imageUrl = extras.getString("url");
setTitle(name);
Glide.with(this).load(imageUrl).into(cityImage);
Bundle bundle = new Bundle();
bundle.putString(TAG,"City Info");
Analystics.getInstance(this).setEvent("City Details",bundle);
}
AllTripActivity
Code:
public class AllTripActivity extends AppCompatActivity {
private RecyclerView tripList;
private static final String TAG= AllTripActivity.class.getName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alltrips);
init();
}
private void init() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
tripList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true));
tripList.setAdapter(new TripAdapter(list, (item) -> {
Intent intent = new Intent(this, PolylineActivity.class);
startActivity(intent);
}));
}
}
TripAdapter
Code:
public class TripAdapter extends RecyclerView.Adapter<TripAdapter.ViewHolder> {
private List<TripModel> list;
private ItemTripBinding mBinding;
private OnItemClickListener<TripModel> mOnItemClickListener;
public TripAdapter(List<TripModel> list, OnItemClickListener<TripModel> onItemClickListener) {
this.list = list;
this.mOnItemClickListener = onItemClickListener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
R.layout.item_trip, parent, false);
return new ViewHolder(mBinding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(list.get(position), mOnItemClickListener);
}
@Override
public int getItemCount() {
return list.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
private ItemTripBinding cityBinding;
public ViewHolder(@NonNull ItemTripBinding cityBinding) {
super(cityBinding.getRoot());
this.cityBinding = cityBinding;
}
public void bind(TripModel item, OnItemClickListener<TripModel> listener) {
cityBinding.setData(item);
itemView.setOnClickListener(v -> listener.onItemClick(item));
}
}
}
WeatherDetailActivity
In this screen, I have implemented the weather related information so that users can identify the city weather condition.
Code:
public class WeatherDetailActivity extends AppCompatActivity {
private ActivityWeatherBinding mMainBinding;
private WeatherViewModel mWeatherViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_weather);
mWeatherViewModel = ViewModelProviders.of(this).get(WeatherViewModel.class);
Bundle extras = getIntent().getExtras();
if (extras != null) {
String name = extras.getString("name");
mMainBinding.txtCityName.setText(name);
mMainBinding.txtTemperature.setText("21" + "\u00B0");
fetchWeatherDetails(name, false);
setForcastData();
}
}
@SuppressLint("SetTextI18n")
private void fetchWeatherDetails(String cityName, boolean isSearching) {
mWeatherViewModel.getCurrentWeatherLiveData(cityName).observeForever(result -> {
if (result != null) {
mMainBinding.txtCityName.setText(result.getLocation().getName());
mMainBinding.txtTemperature.setText(result.getCurrent().getTemperature() + "\u00B0");
}
});
}
private void setForcastData() {
List<CurrentWeather.Current> currents = new ArrayList<>();
for (int i = 1; i <= 7; i++) {
CurrentWeather.Current current = new CurrentWeather().new Current();
current.setObservationTime("Day");
current.setTemperature(21);
current.setIsDay(url);
currents.add(current);
}
mMainBinding.recycleWeeklyWeather.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true));
mMainBinding.recycleWeeklyWeather.setAdapter(new ForcastAdapter(currents));
}
}
WeatherViewModel
Code:
public class WeatherViewModel extends ViewModel {
private WeatherRepository mWeatherRepository = new WeatherRepository();
private final CompositeDisposable disposables = new CompositeDisposable();
private MutableLiveData<CurrentWeather> mWeatherLiveData = new MutableLiveData<>();
private MutableLiveData<ForcastWeather> mForcastWeatherLiveData = new MutableLiveData<>();
public LiveData<CurrentWeather> getCurrentWeatherLiveData(String city) {
disposables.add(mWeatherRepository.executeCurrentWeatherApi(city)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> mWeatherLiveData.setValue(result),
throwable -> mWeatherLiveData.setValue(null)
));
return mWeatherLiveData;
}
public LiveData<ForcastWeather> getForcastWeatherLiveData(String city) {
disposables.add(mWeatherRepository.executeForcastWeatherApi(city)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> mForcastWeatherLiveData.setValue(result),
throwable -> mForcastWeatherLiveData.setValue(null)
));
return mForcastWeatherLiveData;
}
@Override
protected void onCleared() {
disposables.clear();
}
}
Launch the application
Let us launch our application, see the result
Article Introduction
In this article we will work in integrate search and will explore many features together in this service.
Search Kit
HUAWEI Search Kit fully opens Petal Search capabilities through the device-side SDK and cloud-side APIs, enabling ecosystem partners to quickly provide the optimal mobile app search experience.
Dependencies that needed
Code:
//design
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.10.0'
//rxJava
implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
//searchKit HMS
implementation 'com.huawei.hms:searchkit:5.0.4.303'
1. Create a class that extends from Application
Code:
import android.app.Application
import com.huawei.hms.searchkit.SearchKitInstance
class SearchKitApplication: Application() {
override fun onCreate() {
super.onCreate()
// Initialize Search Kit.
SearchKitInstance.init(this, "your_app_id");
}
}
In Manifest in Application tag apply this below line of code
Code:
android:name=".SearchKitApplication"
3. Now we can create an empty activity with this name
SearchActivity. please take a look on important part in references.
or as you like you can modify it, if you would like.
4. In Utils package that we created in point 2, let’s create AnimationUtils class
Code:
public class AnimationUtils {
public static void expand(final View v) {
int matchParentMeasureSpec =
View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getWidth(), View.MeasureSpec.EXACTLY);
int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
v.measure(matchParentMeasureSpec, wrapContentMeasureSpec);
final int targetHeight = v.getMeasuredHeight();
v.getLayoutParams().height = 1;
v.setVisibility(View.VISIBLE);
Animation a =
new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
v.getLayoutParams().height =
interpolatedTime == 1
? ViewGroup.LayoutParams.WRAP_CONTENT
: (int) (targetHeight * interpolatedTime);
v.requestLayout();
}
@Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
public static void collapse(final View v) {
final int initialHeight = v.getMeasuredHeight();
Animation a =
new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1) {
v.setVisibility(View.GONE);
} else {
v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime);
v.requestLayout();
}
}
@Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
}
5. Now in network package let’s work on it to can get access token that will let us able to use search kit features
Code:
public class UrlHelper {
/**
* The Content REQUEST_TOKEN.
*/
public static final String REQUEST_TOKEN = "oauth2/v3/token";
// public static final String REQUEST_TOKEN = "oauth2/v2/token/https://logintestlf.hwcloudtest.cn/";
}
Code:
public interface QueryService {
@FormUrlEncoded
@POST(UrlHelper.REQUEST_TOKEN)
Observable<TokenResponse> getRequestToken(
@Field("grant_type") String grantType,
@Field("client_id") String ClientId,
@Field("client_secret") String clientSecret);
}
Code:
public class NetworkManager {
private static final String TAG = NetworkManager.class.getSimpleName();
private static NetworkManager networkManager;
public static NetworkManager getInstance() {
if (networkManager == null) {
syncInit();
}
return networkManager;
}
private static synchronized void syncInit() {
if (networkManager == null) {
networkManager = new NetworkManager();
}
}
public QueryService createService(Context context, String baseUrl) {
QueryService queryService = null;
Retrofit retrofit = null;
Retrofit.Builder builder = new Retrofit.Builder().baseUrl(baseUrl);
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
try {
SSLSocketFactory ssf = SecureSSLSocketFactory.getInstance(context);
X509TrustManager xtm = new SecureX509TrustManager(context);
clientBuilder.sslSocketFactory(ssf, xtm);
clientBuilder.hostnameVerifier(new StrictHostnameVerifier());
} catch (IOException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (IllegalAccessException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (KeyManagementException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (KeyStoreException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (CertificateException e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
} catch (Exception e) {
Log.e(TAG, "getRetrofit: " + e.getMessage());
}
OkHttpClient client =
clientBuilder
.retryOnConnectionFailure(true)
.readTimeout(5000, TimeUnit.MILLISECONDS)
.connectTimeout(5000, TimeUnit.MILLISECONDS)
.build();
try {
retrofit =
builder.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
queryService = retrofit.create(QueryService.class);
} catch (Exception e) {
Log.e(TAG, "createRestClient error: " + e.getMessage());
}
return queryService;
}
}
6. In bean package, let’s start with create ListBean class
Code:
public class ListBean {
String title;
String url;
String click_url;
public String getClick_url() {
return click_url;
}
public void setClick_url(String click_url) {
this.click_url = click_url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
Now we will work on create TokenResponse class:
Code:
public class TokenResponse {
String access_token;
Integer expires_in;
String token_type;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public Integer getExpires_in() {
return expires_in;
}
public void setExpires_in(Integer expires_in) {
this.expires_in = expires_in;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
}
More details, you can visit https://forums.developer.huawei.com/forumPortal/en/topic/0204411812493980212
Hi i was following your sample, How can I obtain app id?
SearchKitInstance.init(this, "your_app_id");
Please help me
Introduction
Hello reader, in this article, I am going to demonstrate how to utilize Huawei Mobile Services (HMS) Search Kit to search for news articles from the web with customizable parameters. Also, I will show you how to use tools like auto suggestions and spellcheck capabilities provided by HMS Search Kit.
Getting Started
First, we need to follow instructions on the official website to integrate Search Kit into our app.
Getting Started
After we’re done with that, let’s start coding. First, we need to initialize Search Kit in our Application/Activity.
Java:
@HiltAndroidApp
class NewsApp : Application() {
override fun onCreate() {
super.onCreate()
SearchKitInstance.init(this, YOUR_APP_ID)
}
}
Next, let’s not forget adding our Application class to manifest. Also to allow HTTP network requests on devices with targetSdkVersion 28 or later, we need to allow clear text traffic. (Search Kit doesn’t support minSdkVersion below 24).
XML:
<application
android:name=".NewsApp"
android:usesCleartextTraffic="true">
...
</application>
Acquiring Access Token
The token is used to verify a search request on the server. Search results of the request are returned only after the verification is successful. Therefore, before we implement any search functions, we need to get the Access Token first.
OAuth 2.0-based Authentication
If you scroll down, you will see a method called Client Credentials, which does not require authorization from a user. In this mode, your app can generate an access token to access Huawei public app-level APIs. Exactly what we need. I have used Retrofit to do this job.
Let’s create a data class that represents the token response from Huawei servers.
Java:
data class TokenResponse(val access_token: String, val expires_in: Int, val token_type: String)
Then, let’s create an interface like below to generate Retrofit Service.
Java:
interface TokenRequestService {
@FormUrlEncoded
@POST("oauth2/v3/token")
suspend fun getRequestToken(
@Field("grant_type") grantType: String,
@Field("client_id") clientId: String,
@Field("client_secret") clientSecret: String
): TokenResponse
}
Then, let’s create a repository class to call our API service.
Java:
class NewsRepository(
private val tokenRequestService: TokenRequestService
) {
suspend fun getRequestToken() = tokenRequestService.getRequestToken(
"client_credentials",
YOUR_APP_ID,
YOUR_APP_SECRET
)
}
You can find your App ID and App secret from console.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
I have used Dagger Hilt to provide Repository for view models that need it. Here is the Repository Module class that creates the objects to be injected to view models.
Java:
@InstallIn(SingletonComponent::class)
@Module
class RepositoryModule {
@Provides
@Singleton
fun provideRepository(
tokenRequestService: TokenRequestService
): NewsRepository {
return NewsRepository(tokenRequestService)
}
@Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
@Provides
@Singleton
fun providesRetrofitClientForTokenRequest(okHttpClient: OkHttpClient): TokenRequestService {
val baseUrl = "https://oauth-login.cloud.huawei.com/"
return Retrofit.Builder()
.baseUrl(baseUrl)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(TokenRequestService::class.java)
}
}
In order to inject our module, we need to add @HiltAndroidApp annotation to NewsApp application class. Also, add @AndroidEntryPoint to fragments that need dependency injection. Now we can use our repository in our view models.
I have created a splash fragment to get access token, because without it, none of the search functionalities would work.
Java:
@AndroidEntryPoint
class SplashFragment : Fragment(R.layout.fragment_splash) {
private var _binding: FragmentSplashBinding? = null
private val binding get() = _binding!!
private val viewModel: SplashViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSplashBinding.bind(view)
lifecycleScope.launch {
viewModel.accessToken.collect {
if (it is TokenState.Success) {
findNavController().navigate(R.id.action_splashFragment_to_homeFragment)
}
if (it is TokenState.Failure) {
binding.progressBar.visibility = View.GONE
binding.tv.text = "An error occurred, check your connection"
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Java:
class SplashViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var _accessToken = MutableStateFlow<TokenState>(TokenState.Loading)
var accessToken: StateFlow<TokenState> = _accessToken
init {
getRequestToken()
}
private fun getRequestToken() {
viewModelScope.launch {
try {
val token = repository.getRequestToken().access_token
SearchKitInstance.getInstance()
.setInstanceCredential(token)
SearchKitInstance.instance.newsSearcher.setTimeOut(5000)
Log.d(
TAG,
"SearchKitInstance.instance.setInstanceCredential done $token"
)
_accessToken.emit(TokenState.Success(token))
} catch (e: Exception) {
Log.e(HomeViewModel.TAG, "get token error", e)
_accessToken.emit(TokenState.Failure(e))
}
}
}
companion object {
const val TAG = "SplashViewModel"
}
}
As you can see, once we receive our access token, we call setInstanceCredential() method with the token as the parameter. Also I have set a 5 second timeout for the News Searcher. Then, Splash Fragment should react to the change in access token flow, navigate the app to the home fragment while popping splash fragment from back stack, because we don’t want to go back there. But if token request fails, the fragment will show an error message.
Setting up Search Kit Functions
Since we have given Search Kit the token it requires, we can proceed with the rest. Let’s add three more function to our repository.
1. getNews()
This function will take two parameters — search term, and page which will be used for pagination. NewsState is a sealed class that represents two states of news search request, success or failure.
Search Kit functions are synchronous, therefore we launch them in in the Dispatchers.IO context so they don’t block our UI.
In order to start a search request, we create an CommonSearchRequest, then apply our search parameters. setQ to set search term, setLang to set in which language we want to get our news (I have selected English), setSregion to set from which region we want to get our news (I have selected whole world), setPs to set how many news we want in single page, setPn to set which page of news we want to get.
Then we call the search() method to get a response from the server. if it is successful, we get a result in the type of BaseSearchResponse<List<NewsItem>>. If it’s unsuccessful (for example there is no network connection) we get null in return. In that case It returns failure state.
Java:
class NewsRepository(
private val tokenRequestService: TokenRequestService
) {
...
suspend fun getNews(query: String, pageNumber: Int): NewsState = withContext(Dispatchers.IO) {
var newsState: NewsState
Log.i(TAG, "getting news $query $pageNumber")
val commonSearchRequest = CommonSearchRequest()
commonSearchRequest.setQ(query)
commonSearchRequest.setLang(Language.ENGLISH)
commonSearchRequest.setSregion(Region.WHOLEWORLD)
commonSearchRequest.setPs(10)
commonSearchRequest.setPn(pageNumber)
try {
val result = SearchKitInstance.instance.newsSearcher.search(commonSearchRequest)
newsState = if (result != null) {
if (result.data.size > 0) {
Log.i(TAG, "got news ${result.data.size}")
NewsState.Success(result.data)
} else {
NewsState.Error(Exception("no more news"))
}
} else {
NewsState.Error(Exception("fetch news error"))
}
} catch (e: Exception) {
newsState = NewsState.Error(e)
Log.e(TAG, "caught news search exception", e)
}
[email protected] newsState
}
suspend fun getAutoSuggestions(str: String): AutoSuggestionsState =
withContext(Dispatchers.IO) {
val autoSuggestionsState: AutoSuggestionsState
autoSuggestionsState = try {
val result = SearchKitInstance.instance.searchHelper.suggest(str, Language.ENGLISH)
if (result != null) {
AutoSuggestionsState.Success(result.suggestions)
} else {
AutoSuggestionsState.Failure(Exception("fetch suggestions error"))
}
} catch (e: Exception) {
AutoSuggestionsState.Failure(e)
}
[email protected] autoSuggestionsState
}
suspend fun getSpellCheck(str: String): SpellCheckState = withContext(Dispatchers.IO) {
val spellCheckState: SpellCheckState
spellCheckState = try {
val result = SearchKitInstance.instance.searchHelper.spellCheck(str, Language.ENGLISH)
if (result != null) {
SpellCheckState.Success(result)
} else {
SpellCheckState.Failure(Exception("fetch spellcheck error"))
}
} catch (
e: Exception
) {
SpellCheckState.Failure(e)
}
[email protected] spellCheckState
}
companion object {
const val TAG = "NewsRepository"
}
}
2. getAutoSuggestions()
Search Kit can provide search suggestions with SearchHelper.suggest() method. It takes two parameters, a String to provide suggestions for, and a language type. If the operation is successful, a result in the type AutoSuggestResponse. We can access a list of SuggestObject from suggestions field of this AutoSuggestResponse. Every SuggestObject represents a suggestion from HMS which contains a String value.
3. getSpellCheck()
It works pretty much the same with auto suggestions. SearchHelper.spellCheck() method takes the same two parameters like suggest() method. But it returns a SpellCheckResponse, which has two important fields: correctedQuery and confidence. correctedQuery is what Search Kit thinks the corrected spelling should be, confidence is how confident Search kit is about the recommendation. Confidence has 3 values, which are 0 (not confident, we should not rely on it), 1 (confident), 2 (highly confident).
Using the functions above in our app
Home Fragments has nothing to show when it launches, because nothing has been searched yet. User can click the magnifier icon in toolbar to navigate to Search Fragment. Code for Search Fragment/View Model is below.
Notes:
Search View should expand on default with keyboard showing so user can start typing right away.
Every time query text changes, it will be emitted to a flow in view model. then it will be collected by two listeners in the fragment, first one to search for auto suggestions, second one to spell check. I did this to avoid unnecessary network calls, debounce(500) will make sure subsequent entries when the user is typing fast (less than half a second for a character) will be ignored and only the last search query will be used.
When user submit query term, the string will be sent back to HomeFragment using setFragmentResult() (which is only available fragment-ktx library Fragment 1.3.0-alpha04 and above).
Java:
@AndroidEntryPoint
class SearchFragment : Fragment(R.layout.fragment_search) {
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private val viewModel: SearchViewModel by viewModels()
@FlowPreview
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSearchBinding.bind(view)
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
setHasOptionsMenu(true)
//listen to the change in query text, trigger getSuggestions function after debouncing and filtering
lifecycleScope.launch {
viewModel.searchQuery.debounce(500).filter { s: String ->
[email protected] s.length > 3
}.distinctUntilChanged().flatMapLatest { query ->
Log.d(TAG, "getting suggestions for term: $query")
viewModel.getSuggestions(query).catch {
}
}.flowOn(Dispatchers.Default).collect {
if (it is AutoSuggestionsState.Success) {
val list = it.data
Log.d(TAG, "${list.size} suggestion")
binding.chipGroup.removeAllViews()
//create a chip for each suggestion and add them to chip group
list.forEach { suggestion ->
val chip = Chip(requireContext())
chip.text = suggestion.name
chip.isClickable = true
chip.setOnClickListener {
//set fragment result to return search term to home fragment.
setFragmentResult(
"requestKey",
bundleOf("bundleKey" to suggestion.name)
)
findNavController().popBackStack()
}
binding.chipGroup.addView(chip)
}
} else if (it is AutoSuggestionsState.Failure) {
Log.e(TAG, "suggestions request error", it.exception)
}
}
}
//listen to the change in query text, trigger spellcheck function after debouncing and filtering
lifecycleScope.launch {
viewModel.searchQuery.debounce(500).filter { s: String ->
[email protected] s.length > 3
}.distinctUntilChanged().flatMapLatest { query ->
Log.d(TAG, "spellcheck for term: $query")
viewModel.getSpellCheck(query).catch {
Log.e(TAG, "spellcheck request error", it)
}
}.flowOn(Dispatchers.Default).collect {
if (it is SpellCheckState.Success) {
val spellCheckResponse = it.data
val correctedStr = spellCheckResponse.correctedQuery
val confidence = spellCheckResponse.confidence
Log.d(
TAG,
"corrected query $correctedStr confidence level $confidence"
)
if (confidence > 0) {
//show spellcheck layout, and set on click listener to send corrected term to home fragment
//to be searched
binding.tvDidYouMeanToSearch.visibility = View.VISIBLE
binding.tvCorrected.visibility = View.VISIBLE
binding.tvCorrected.text = correctedStr
binding.llSpellcheck.setOnClickListener {
setFragmentResult(
"requestKey",
bundleOf("bundleKey" to correctedStr)
)
findNavController().popBackStack()
}
} else {
binding.tvDidYouMeanToSearch.visibility = View.GONE
binding.tvCorrected.visibility = View.GONE
}
} else if (it is SpellCheckState.Failure) {
Log.e(TAG, "spellcheck request error", it.exception)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_search, menu)
val searchMenuItem = menu.findItem(R.id.searchItem)
val searchView = searchMenuItem.actionView as SearchView
searchView.setIconifiedByDefault(false)
searchMenuItem.expandActionView()
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
findNavController().popBackStack()
return true
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return if (query != null && query.length > 3) {
setFragmentResult("requestKey", bundleOf("bundleKey" to query))
findNavController().popBackStack()
true
} else {
Toast.makeText(requireContext(), "Search term is too short", Toast.LENGTH_SHORT)
.show()
true
}
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.emitNewTextToSearchQueryFlow(newText ?: "")
return true
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "SearchFragment"
}
}
Java:
class SearchViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var _searchQuery = MutableStateFlow<String>("")
var searchQuery: StateFlow<String> = _searchQuery
fun getSuggestions(str: String): Flow<AutoSuggestionsState> {
return flow {
try {
val result = repository.getAutoSuggestions(str)
emit(result)
} catch (e: Exception) {
}
}
}
fun getSpellCheck(str: String): Flow<SpellCheckState> {
return flow {
try {
val result = repository.getSpellCheck(str)
emit(result)
} catch (e: Exception) {
}
}
}
fun emitNewTextToSearchQueryFlow(str: String) {
viewModelScope.launch {
_searchQuery.emit(str)
}
}
}
Now the HomeFragment has a search term to search for.
When the view is created, we receive the search term returned from Search Fragment on setFragmentResultListener. Then search for news using this query, then submit the PagingData to the recycler view adapter. Also, I made sure same flow will be returned if the new query is the same with the previous one so no unnecessary calls will be made.
Java:
@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
private lateinit var listAdapter: NewsAdapter
private var startedLoading = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
setHasOptionsMenu(true)
listAdapter = NewsAdapter(NewsAdapter.NewsComparator, onItemClicked)
binding.rv.adapter =
listAdapter.withLoadStateFooter(NewsLoadStateAdapter(listAdapter))
//if user swipe down to refresh, refresh paging adapter
binding.swipeRefreshLayout.setOnRefreshListener {
listAdapter.refresh()
}
// Listen to search term returned from Search Fragment
setFragmentResultListener("requestKey") { _, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
binding.tv.visibility = View.GONE
if (result != null) {
binding.toolbar.subtitle = "News about $result"
lifecycleScope.launchWhenResumed {
binding.swipeRefreshLayout.isRefreshing = true
viewModel.searchNews(result).collectLatest { value: PagingData<NewsItem> ->
listAdapter.submitData(value)
}
}
}
}
//need to listen to paging adapter load state to stop swipe to refresh layout animation
//if load state contain error, show a toast.
listAdapter.addLoadStateListener {
if (it.refresh is LoadState.NotLoading && startedLoading) {
binding.swipeRefreshLayout.isRefreshing = false
} else if (it.refresh is LoadState.Error && startedLoading) {
binding.swipeRefreshLayout.isRefreshing = false
val loadState = it.refresh as LoadState.Error
val errorMsg = loadState.error.localizedMessage
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
} else if (it.refresh is LoadState.Loading) {
startedLoading = true
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_home, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.searchItem -> {
//launch search fragment when search item clicked
findNavController().navigate(R.id.action_homeFragment_to_searchFragment)
true
}
else ->
super.onOptionsItemSelected(item)
}
}
//callback function to be passed to paging adapter, used to launch news links.
private val onItemClicked = { it: NewsItem ->
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(requireContext(), Uri.parse(it.clickUrl))
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "HomeFragment"
}
}
Java:
class HomeViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var lastSearchQuery: String? = null
var lastFlow: Flow<PagingData<NewsItem>>? = null
fun searchNews(query: String): Flow<PagingData<NewsItem>> {
return if (query != lastSearchQuery) {
lastSearchQuery = query
lastFlow = Pager(PagingConfig(pageSize = 10)) {
NewsPagingDataSource(repository, query)
}.flow.cachedIn(viewModelScope)
lastFlow as Flow<PagingData<NewsItem>>
} else {
lastFlow!!
}
}
companion object {
const val TAG = "HomeViewModel"
}
}
The app also uses Paging 3 library to provide endless scrolling for news articles, which is out of scope for this article, you may check the GitHub repo for how to achieve pagination with Search Kit. The end result looks like the images below.
Check the repo here.
Tips
When Search Kit fails to fetch results (example: no internet connection), it will return null object, you can manually return an exception so you can handle the error.
Conclusion
HMS Search Kit provide easy to use APIs for fast and efficient customizable searching for web sites, images, videos and news articles in many languages and regions. Also, it provides convenient features like auto suggestions and spellchecking.
Reference
Huawei Search Kit
What other features search kit provides other than news?
any additional feature can be supported?
Can we search daily base news ?
ask011 said:
What other features search kit provides other than news?
Click to expand...
Click to collapse
Hello, can reference documentation at https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
ask011 said:
What other features search kit provides other than news?
Click to expand...
Click to collapse
Hello, can reference documentation at https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
I am trying to access the googleFit API.
It seems pretty straightforward. Get the google sign-in permissions and required authorizations then query for Step count.
My code doesn't seem to work.
and it logs only "Error!!" that's it.
Android gurus, Where am I going wrong??
Code:
fun getAuthorizationAndReadData() {
try {
MainActivity().fitSignIn(FitActionRequestCode.READ_DATA)
} catch () {
Log.i("e", "error!!!!")
}
}
MainActivity.kt
Code:
enum class FitActionRequestCode {
READ_DATA
}
private val fitnessOptions: GoogleSignInOptionsExtension = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ).build()
fun fitSignIn(requestCode: FitActionRequestCode) {
if (oAuthPermissionsApproved()) {
readHistoryData()
} else {
requestCode.let {
GoogleSignIn.requestPermissions(
this,
requestCode.ordinal,
getGoogleAccount(), fitnessOptions)
}
}
}
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
private fun performActionForRequestCode(requestCode: FitActionRequestCode) = when (requestCode) {
FitActionRequestCode.READ_DATA -> readHistoryData()
}
private fun readHistoryData(): Task<DataReadResponse> {
// Begin by creating the query.
val readRequest = queryFitnessData()
// Invoke the History API to fetch the data with the query
return Fitness.getHistoryClient(this, getGoogleAccount())
.readData(readRequest)
.addOnSuccessListener { dataReadResponse ->
printData(dataReadResponse)
Log.i(ContentValues.TAG, "Data read was successful!") }
.addOnFailureListener { e ->
Log.e(ContentValues.TAG, "There was a problem reading the data.", e)
}
}
private fun queryFitnessData(): DataReadRequest {
// [START build_read_data_request]
// Setting a start and end date using a range of 1 week before this moment.
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val now = Date()
calendar.time = now
val endTime = calendar.timeInMillis
calendar.add(Calendar.WEEK_OF_YEAR, -1)
val startTime = calendar.timeInMillis
return DataReadRequest.Builder()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
.bucketByTime(1, TimeUnit.DAYS)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
}
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Huawei Search Kit includes device-side SDK and cloud-side APIs to use all features of Petal Search capabilities. It helps developers to integrate mobile app search experience into their application.
Huawei Search Kit offers to developers so much different and helpful features. It decreases our development cost with SDKs and APIs, it returns responses quickly and it helps us to develop our application faster.
As a developer, we have some responsibilities and function restrictions while using Huawei Search Kit. If you would like to learn about these responsibilities and function restrictions, I recommend you to visit following website.
https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
Also, Huawei Search Kit supports limited countries and regions. If you wonder about these countries and regions, you can visit the following website.
https://developer.huawei.com/consum...HMSCore-Guides-V5/regions-0000001056871703-V5
How to use Huawei Search Kit?
First of all, we need to create an app on AppGallery Connect and add related details about HMS Core to our project.
If you don’t know about how to integrate HMS Core to our project, you can learn all details from following Medium article.
https://medium.com/huawei-developers/android-integrating-your-apps-with-huawei-hms-core-1f1e2a090e98
After we have done all steps in above Medium article, we can focus on special steps of integrating Huawei Search Kit.
Our minSdkVersion should be 24 at minimum.
We need to add following dependency to our app level build.gradle file.
Code:
implementation "com.huawei.hms:searchkit:5.0.4.303"
Then, we need to do some changes on AppGallery Connect. We need to define a data storage location on AppGallery Connect.
Note: If we don’t define a data storage location, all responses will return null.
We need to initialize the SearchKit instance on our application which we have extended from android.app.Application class. To initialize the SearchKit instance, we need to set the app id on second parameter which has mentioned as Constants.APP_ID.
While adding our application class to AndroidManifest.xml file, we need to set android:usesCleartextTraffic as true. You can do all these steps as mentioned in red rectangles.
Getting Access Token
For each request on Search Kit, we need to use access token. I prefer to get this access token on splash screen of the application. Thus, we will be able to save access token and save it with SharedPreferences.
First of all, we need to create our methods and objects about network operations. I am using Koin Framework for dependency injection on this project.
For creating objects about network operations, I have created following single objects and methods.
Note: In above picture, I have initialized the koin framework and added network module. Check this step to use this module in the app.
Java:
val networkModule = module {
single { getOkHttpClient(androidContext()) }
single { getRetrofit(get()) }
single { getService<AccessTokenService>(get()) }
}
fun getRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder().baseUrl("https://oauth-login.cloud.huawei.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun getOkHttpClient(context: Context): OkHttpClient {
return OkHttpClient().newBuilder()
.sslSocketFactory(SecureSSLSocketFactory.getInstance(context), SecureX509TrustManager(context))
.hostnameVerifier(StrictHostnameVerifier())
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(1, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
inline fun <reified T> getService(retrofit: Retrofit): T = retrofit.create(T::class.java)
We have defined methods to create OkHttpClient and Retrofit objects. These objects have used as single to create Singleton objects. Also, we have defined one generic method to use Retrofit with our services.
To get an access token, our base URL will be “https://oauth-login.cloud.huawei.com/".
To get response from access token request, we need to define an object for response. The best way to do that is creating data class which is as shown in the below.
Java:
data class AccessTokenResponse(
@SerializedName("access_token") val accessToken: String?,
@SerializedName("expires_in") val expiresIn: Int?,
@SerializedName("token_type") val tokenType: String?
)
Now, all we need to do is, creating an interface to send requests with Retrofit. To get access token, our total URL is “https://oauth-login.cloud.huawei.com/oauth2/v3/token". We need to send 3 parameters as x-www-form-url encoded. Let’s examine these parameters.
grant_type: This parameter will not change depends on our application. Value should be, “client_credentials”.
client_id: This parameter will be app id of our project.
client_secret: This parameter will be app secret of our project.
Java:
interface AccessTokenService {
@FormUrlEncoded
@POST("oauth2/v3/token")
fun getAccessToken(
@Field("grant_type") grantType: String,
@Field("client_id") appId: String,
@Field("client_secret") clientSecret: String
): Call<AccessTokenResponse>
}
Now, everything is ready to get an access token. We just need to send the request and save the access token with SharedPreferences.
To work with SharedPreferences, I have created a helper class as shown in the below.
Java:
class CacheHelper {
companion object {
private lateinit var instance: CacheHelper
private var gson: Gson = Gson()
private const val PREFERENCES_NAME = BuildConfig.APPLICATION_ID
private const val PREFERENCES_MODE = AppCompatActivity.MODE_PRIVATE
fun getInstance(context: Context): CacheHelper {
instance = CacheHelper(context)
return instance
}
}
private var context: Context
private var sharedPreferences: SharedPreferences
private var sharedPreferencesEditor: SharedPreferences.Editor
private constructor(context: Context) {
this.context = context
sharedPreferences = this.context.getSharedPreferences(PREFERENCES_NAME, PREFERENCES_MODE)
sharedPreferencesEditor = sharedPreferences.edit()
}
fun putObject(key: String, `object`: Any) {
sharedPreferencesEditor.apply {
putString(key, gson.toJson(`object`))
commit()
}
}
fun <T> getObject(key: String, `object`: Class<T>): T? {
return sharedPreferences.getString(key, null)?.let {
gson.fromJson(it, `object`)
} ?: kotlin.run {
null
}
}
}
With the help of this class, we will be able to work with SharedPreferences easier.
Now, all we need to do it, sending request and getting access token.
Java:
object SearchKitService: KoinComponent {
private val accessTokenService: AccessTokenService by inject()
private val cacheHelper: CacheHelper by inject()
fun initAccessToken(requestListener: IRequestListener<Boolean, Boolean>) {
accessTokenService.getAccessToken(
"client_credentials",
Constants.APP_ID,
Constants.APP_SECRET
).enqueue(object: retrofit2.Callback<AccessTokenResponse> {
override fun onResponse(call: Call<AccessTokenResponse>, response: Response<AccessTokenResponse>) {
response.body()?.accessToken?.let { accessToken ->
cacheHelper.putObject(Constants.ACCESS_TOKEN_KEY, accessToken)
requestListener.onSuccess(true)
} ?: kotlin.run {
requestListener.onError(true)
}
}
override fun onFailure(call: Call<AccessTokenResponse>, t: Throwable) {
requestListener.onError(false)
}
})
}
}
If API returns as access token successfully, we will save this access token to device using SharedPreferences. And on our SplashFragment, we need to listen IRequestListener and if onSuccess method returns true, that means we got the access token successfully and we can navigate application to BrowserFragment.
Huawei Search Kit
In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.
In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.
To send requests for News Search, Image Search and Video Search, we need a CommonSearchRequest object.
In this app, I will get results about Corona in English. I have created the following method to return to CommonSearchRequest object.
Java:
private fun returnCommonRequest(): CommonSearchRequest {
return CommonSearchRequest().apply {
setQ("Corona Virus")
setLang(Language.ENGLISH)
setSregion(Region.WHOLEWORLD)
setPs(20)
setPn(1)
}
}
Here, we have setted some informations. Let’s examine this setter methods.
setQ(): Setting the keyword for search.
setLang(): Setting the language for search. Search Kit has it’s own model for language. If you would like examine this enum and learn about which Languages are supporting by Search Kit, you can visit the following website.
Huawei Search Kit — Language Model
setSregion(): Setting the region for search. Search Kit has it’s own model for region. If you would like examine this enum and learn about which Regions are supporting by Search Kit, you can visit the following website.
Huawei Search Kit — Region Model
setPn(): Setting the number about how much items will be in current page. The value ranges from 1 to 100, and the default value is 1.
setPs(): Setting the number of search results that will be returned on a page. The value ranges from 1 to 100, and the default value is 10.
Now, all we need to do is getting news, images, videos and show the results for these on the screen.
News Search
To get news, we can use the following method.
Java:
fun newsSearch(requestListener: IRequestListener<List<NewsItem>, String>) {
SearchKitInstance.getInstance().newsSearcher.setCredential(SearchKitService.accessToken)
var newsList = SearchKitInstance.getInstance().newsSearcher.search(SearchKitService.returnCommonRequest())
newsList?.getData()?.let { newsItems ->
requestListener.onSuccess(newsItems)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Image Search
To get images, we can use the following method.
Java:
fun imageSearch(requestListener: IRequestListener<List<ImageItem>, String>) {
SearchKitInstance.getInstance().imageSearcher.setCredential(SearchKitService.accessToken)
var imageList = SearchKitInstance.getInstance().imageSearcher.search(SearchKitService.returnCommonRequest())
imageList?.getData()?.let { imageItems ->
requestListener.onSuccess(imageItems)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Video Search
To get images, we can use the following method.
Java:
fun videoSearch(requestListener: IRequestListener<List<VideoItem>, String>) {
SearchKitInstance.getInstance().videoSearcher.setCredential(SearchKitService.accessToken)
var videoList = SearchKitInstance.getInstance().videoSearcher.search(SearchKitService.returnCommonRequest())
videoList?.getData()?.let { videoList ->
requestListener.onSuccess(videoList)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Showing on screen
All these results return a clickable url for each one. We can create an intent to open these URLs on the browser which has installed to device before.
To do that and other operations, I will share BrowserFragment codes for fragment and the SearchItemAdapter codes for recyclerview.
Java:
class BrowserFragment: Fragment() {
private lateinit var viewBinding: FragmentBrowserBinding
private lateinit var searchOptionsTextViews: ArrayList<TextView>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewBinding = FragmentBrowserBinding.inflate(inflater, container, false)
searchOptionsTextViews = arrayListOf(viewBinding.news, viewBinding.images, viewBinding.videos)
return viewBinding.root
}
private fun setListeners() {
viewBinding.news.setOnClickListener { getNews() }
viewBinding.images.setOnClickListener { getImages() }
viewBinding.videos.setOnClickListener { getVideos() }
}
private fun getNews() {
SearchKitService.newsSearch(object: IRequestListener<List<NewsItem>, String>{
override fun onSuccess(newsItemList: List<NewsItem>) {
setupRecyclerView(newsItemList, viewBinding.news)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private fun getImages(){
SearchKitService.imageSearch(object: IRequestListener<List<ImageItem>, String>{
override fun onSuccess(imageItemList: List<ImageItem>) {
setupRecyclerView(imageItemList, viewBinding.images)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private fun getVideos() {
SearchKitService.videoSearch(object: IRequestListener<List<VideoItem>, String>{
override fun onSuccess(videoItemList: List<VideoItem>) {
setupRecyclerView(videoItemList, viewBinding.videos)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private val clickListener = object: IClickListener<String> {
override fun onClick(clickedInfo: String) {
var intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(clickedInfo)
}
startActivity(intent)
}
}
private fun <T> setupRecyclerView(itemList: List<T>, selectedSearchOption: TextView) {
viewBinding.searchKitRecyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = SearchItemAdapter<T>(itemList, clickListener)
}
changeSelectedTextUi(selectedSearchOption)
}
private fun changeSelectedTextUi(selectedSearchOption: TextView) {
for (textView in searchOptionsTextViews)
if (textView == selectedSearchOption) {
textView.background = requireContext().getDrawable(R.drawable.selected_text)
} else {
textView.background = requireContext().getDrawable(R.drawable.unselected_text)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setListeners()
getNews()
}
}
Java:
class SearchItemAdapter<T>(private val searchItemList: List<T>,
private val clickListener: IClickListener<String>):
RecyclerView.Adapter<SearchItemAdapter.SearchItemHolder<T>>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchItemHolder<T> {
val itemBinding = ItemSearchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchItemHolder<T>(itemBinding)
}
override fun onBindViewHolder(holder: SearchItemHolder<T>, position: Int) {
val item = searchItemList[position]
var isLast = (position == searchItemList.size - 1)
holder.bind(item, isLast, clickListener)
}
override fun getItemCount(): Int = searchItemList.size
override fun getItemViewType(position: Int): Int = position
class SearchItemHolder<T>(private val itemBinding: ItemSearchBinding): RecyclerView.ViewHolder(itemBinding.root) {
fun bind(item: T, isLast: Boolean, clickListener: IClickListener<String>) {
if (isLast)
itemBinding.itemSeparator.visibility = View.GONE
lateinit var clickUrl: String
var imageUrl = "https://www.who.int/images/default-source/infographics/who-emblem.png?sfvrsn=877bb56a_2"
when(item){
is NewsItem -> {
itemBinding.searchResultTitle.text = item.title
itemBinding.searchResultDetail.text = item.provider.siteName
clickUrl = item.clickUrl
item.provider.logo?.let { imageUrl = it }
}
is ImageItem -> {
itemBinding.searchResultTitle.text = item.title
clickUrl = item.clickUrl
item.sourceImage.image_content_url?.let { imageUrl = it }
}
is VideoItem -> {
itemBinding.searchResultTitle.text = item.title
itemBinding.searchResultDetail.text = item.provider.siteName
clickUrl = item.clickUrl
item.provider.logo?.let { imageUrl = it }
}
}
itemBinding.searchItemRoot.setOnClickListener {
clickListener.onClick(clickUrl)
}
getImageFromUrl(imageUrl, itemBinding.searchResultImage)
}
private fun getImageFromUrl(url: String, imageView: ImageView) {
Glide.with(itemBinding.root)
.load(url)
.centerCrop()
.into(imageView);
}
}
}
End
If you would like to learn more about Search Kit and see the Codelab, you can visit the following websites:
https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
https://developer.huawei.com/consumer/en/codelab/HMSSearchKit/index.html#0
Very nice guide.
Amazing.
Thank you very much
It's a very nice example.