📜 ⬆️ ⬇️

"Peeped" - the path from the idea for the VK Mobile Challenge to the real product

Three months of driving and overtime. Nervous tension sometimes went off scale, but optimism did not run out. We set ourselves difficult tasks and tried to solve them in a non-standard way. And we did it.




My name is Alexey, I will tell you about our experience of participating in the VC competition on the development of mobile applications for Android, iOS and Windows Phone platforms. I think my article will help beginners to soberly assess their strength and know what awaits them.
')
Competition terms, if interested, here , and a couple of words about our product. We decided to enter the contest with the project “Peeped - the city in the palm of your hand”. We had about 50 public "Peeped" in different cities, but we wanted to create something that unites all the city news in one place. And we got to work. It was necessary to make a functional mobile application in which all the events and news of the city (aggregated from Vkontakte) would be maximally accessible to each user.

For the main indicator of the success of the application “Peeped”, we took the reaction of users (Retention) and an understanding of whether the product will form a new habit of people to use the application.

Presently:


Of course, I want more impressive figures, but for an application that is only 4 months old, these are quite good results.
During these three months that we “sawed” the project, naturally, various difficulties arose. Our developers will share with you the experience of solving problems.

Case from iOS developer


“Difficulties in the work on“ Peeped ”arose enough. Here is one of them. I was tasked with shoving vertically scrolling content into categories that can be switched with a scroll. Do not svaypom, namely scrolling. At first, it was decided to use UIPageViewController. Inner flair did not let me down, after a while there were suspensions with horizontal scrolling. I had to redo the UICollectionView, the cells of which are UIScrollView with content. Thus, I achieved a smooth switching between categories, but the vertical scroll showed no signs of life. I had to rewrite touch processing so that gestures are passed down the hierarchy.

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (!_isHorizontalScroll && !_isMultitouch) { CGPoint point = [[touches anyObject] locationInView:self]; if (fabs(_originalPoint.x - point.x) > kThresholdX && fabs(_originalPoint.y - point.y) < kThresholdY) { _isHorizontalScroll = YES; [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]; } } if (_isHorizontalScroll){ [super touchesMoved:touches withEvent:event]; }else{ [_currentChild touchesMoved:touches withEvent:event]; } } 



Case from Android developer


When choosing the architecture of the application, I stopped at Clean Architecture (https://github.com/android10/Android-CleanArchitecture). This architecture is built on the principles formulated by Bob Martin. It makes no sense to describe the architecture itself and the benefits derived from its use, many excellent articles have been written on this topic (for example: “Architecture of Android Applications ... The Right Way?” And “ Pure Architecture ”), but to understand what is going on further, I advise you to read them. Immediately go to the problem encountered in the development of our application.

“Peeped - the city in the palm of your hand” is a kind of platform for viewing actual information about a particular city. In order to save users from manual search in a large list of cities, we need to determine the current location of the user. At first I worked in the old fashioned way: I used the system LocationManager, I got a list of providers from it, and from them a specific location. The standard way to solve a problem, I think everyone is familiar with it. Everything worked fine. But there were a few problems.

1. The Android API> = 23 introduced Runtime Permossions. This means that on devices with Android 6.0 you need to request some permissions in runtime, in our case, to determine the coordinates. We determine the current location is done immediately - on the first screen. We considered that such a request might scare some users away.

2. LocationManager lay in the presentation layer, which is very much contrary to the principles embodied in Clean Architecture.

To solve the first problem, I resorted to the service from Yandex: Locator (https://tech.yandex.ru/locator/). With this service, you can determine the current location of the nearest Wi-Fi access points and mobile cells, without using GPS. Thus, we will get rid of the annoying dialogue. But this service, for various reasons, does not always produce the correct result. On the site of the service itself is written:

> In some cases, Yandex. Locator reports an accuracy of 100,000 meters, which means that it was not possible to reliably determine the location. This happens if the location is not determined by the IP address of the mobile device, but by the IP address of a public server or proxy server.

In this case, we cannot rely on the result obtained. So we need to go back to the LocationManager. Thus, the algorithm is as follows: We made a request to the Yandex locator, if he did not return anything, or if the accuracy of a specific location is> = 100000 meters, we request it from the system LocationManager.

Let me turn to the solution of the second problem: we are responsible for issuing the current coordinate to the data layer, as it is responsible for managing the data in the application.

But first, go to the domain layer. I created the LocationService interface:

 public interface LocationService { Observable<LocationEntity> getCurrentLocation(); } 

And used it in UseCase:

 public class GetCurrentLocationUseCase extends UseCase<LocationEntity> { public static final String CASE_NAME = "get_current_location"; private final LocationService mLocationService; @Inject public GetCurrentLocationUseCase(LocationService locationService, ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) { super(threadExecutor, postExecutionThread); mLocationService = locationService; } @Override protected Observable<LocationEntity> buildUseCaseObservable() { return mLocationService.getCurrentLocation(); } } 

Now this UseCase can be easily “injected” into Presenter, with the help of Dagger2, thus abstracting from the specific implementation of the service.

 public class YandexLocationService implements LocationService, Constants { public static final int MAX_PRECISION = 100000; private final YandexLocatorService mYandexLocatorService; @Inject public YandexLocationService() { RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(BASE_URL_LOCATOR) .setLogLevel(RETROFIT_LOG_LEVEL) .build(); mYandexLocatorService = restAdapter.create(YandexLocatorService.class); } @Override public Observable<LocationEntity> getCurrentLocation() { return getCurrentLocationByIp() .timeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) .filter(yandexLocation -> yandexLocation.getPrecision() < MAX_PRECISION) .map(LocationTransformer::transformToLocationEntity); } public Observable<YandexLocation> getCurrentLocationByIp() { return mYandexLocatorService.getLocation(getLocatorRequestObject()); } public YandexRequest getLocatorRequestObject() { return new YandexRequest(new Common(LOCATOR_VERSION, LOCATOR_API_KEY)); } } 

In this example, the definition of coordinates is made only by IP address. Here I filter the coordinates with poor accuracy (> = 100000) and convert the resulting YandexLocation entity into LocationEntity. Next, we turn to the system service coordinate determination. It is a little more difficult for him, since he uses Runtime Permissions, and, therefore, he has to request permissions. I made the interface:

 public interface PermissionsRequester { Observable<Boolean> request(String... permissions); } 

We will implement this interface in the presentation layer using the RxPermissions library:

 public class PermissionsRequesterImpl implements PermissionsRequester { private final RxPermissions mRxPermissions; public PermissionsRequesterImpl(Context context) { mRxPermissions = RxPermissions.getInstance(context); } @Override public Observable<Boolean> request(String... permissions) { return mRxPermissions.request(permissions); } } 

Now you can easily use this interface:

 public class SystemLocationService implements LocationService { private final LocationManager mLocationManager; private final PermissionsRequester mPermissionsRequester; @Inject public SystemLocationService(LocationManager locationManager, PermissionsRequester permissionsRequester) { mLocationManager = locationManager; mPermissionsRequester = permissionsRequester; } @Override public Observable<LocationEntity> getCurrentLocation() { return getCurrentGpsLocation() .map(LocationTransformer::transformToLocationEntity); } public Observable<Location> getCurrentGpsLocation() { return mPermissionsRequester .request(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) .flatMap(granted -> granted ? findLastLocation() : Observable.error(new RuntimeException())); } private Observable<Location> findLastLocation() { return Observable.create(new Observable.OnSubscribe<Location>() { @Override public void call(Subscriber<? super Location> subscriber) { Location lastLocation = null; List<String> providers = mLocationManager.getAllProviders(); if (providers != null && !providers.isEmpty()) { for (String provider : providers) { if (mLocationManager.isProviderEnabled(provider)) { Location auxLocation = mLocationManager.getLastKnownLocation(provider); if (auxLocation != null) { if (lastLocation == null) { lastLocation = auxLocation; } else if (auxLocation.getTime() > lastLocation.getTime()) { lastLocation = auxLocation; } } } } } subscriber.onNext(lastLocation); subscriber.onCompleted(); } }); } } 

So I made 2 services. But you need to use both. How to solve this problem? I created a CompositeLocationService, which receives several implementations of the LocationService interface and starts each of them in turn, until I get a specific location:

 public class CompositeLocationService implements LocationService { public static final int DEFAULT_TIMEOUT = 30; private final LocationService[] mLocationServices; @Inject public CompositeLocationService(LocationService... services) { if (services == null || services.length == 0) { throw new CompositeLocationEmptyException(); } mLocationServices = services; } @Override public Observable<LocationEntity> getCurrentLocation() { return Observable.concat( Observable.from(mLocationServices) .map(locationService -> locationService .getCurrentLocation() .onErrorReturn(throwable -> null) .timeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS, Observable.empty()) ) ) .first(location -> location != null); } } 

In the getCurrentLocation method, we sequentially query the services one after another, and if a non-zero result is obtained, we give it out, ignoring the remaining services. Thus, we can use an unlimited number of services. Next, in the presentation layer, combine these classes in the LocationModule module using Dagger2:

 @Module public class LocationModule { final PubApplication mPubApplication; public LocationModule(PubApplication pubApplication) { mPubApplication = pubApplication; } @Provides @Singleton Context providesApplicationContext() { return mPubApplication; } @Provides @Singleton LocationManager providesLocationManager() { return (LocationManager) mPubApplication.getSystemService(Context.LOCATION_SERVICE); } @Provides @Singleton PermissionsRequester providesRxPermissions(Context context) { return new PermissionsRequesterImpl(context); } @Provides @Singleton YandexLocationService provideYandexLocationService() { return new YandexLocationService(); } @Provides @Singleton SystemLocationService provideSystemLocationService(LocationManager locationManager, PermissionsRequester permissionsRequester) { return new SystemLocationService(locationManager, permissionsRequester); } @Provides @Singleton LocationService provideLocationService(YandexLocationService yandexLocationService, SystemLocationService systemLocationService) { return new CompositeLocationService(yandexLocationService, systemLocationService); } } 

All is ready! Now you can safely use GetCurrentLocationUseCase in our presenter. Thus, albeit partially, but some problems with determining the location have been resolved. A dialog with a location request will appear much less frequently, and, therefore, will scare away far fewer users. And from the point of view of architecture, now everything is much better, the presentation layer is responsible for the display, data for the information. This example is not perfect, but I think it allows you to understand the general principle of solving these problems in the context of "pure architecture".



Briefly about technology


To make it clearer, here are some more explanations of what technologies we used. The server part is located on two DigitalOcean servers with a configuration of 1 core / 1Gb RAM.

The backend is written in PHP7 using the Yii2 framework.

1 server - db-master, synchronization script with VKontakte.
2 server - db-slave, rest api, web client.
The web client is written using the AngularJS v1.5.2 framework.
Geolocation is determined using the service Geolocator from Yandex.

And at the end of a bit of statistics. Currently, there are about 3200 groups (public) in the system, synchronization with Vkontakte is carried out in several streams. With the current server configuration, the average queue for updating all groups is 6 minutes. Over the past month, about 2 million new posts have been uploaded. We update these posts only if they are not older than 1 month and not more than 100 last in the group.

And yes, we are proud that among the nearly 130 projects our “Peeped” is one of the few presented on 2 platforms.

Thanks to participation in the competition, we were able to test our strength and take a more objective look at our project. They experienced incredible excitement and drive and even more united the team with one idea.

Source: https://habr.com/ru/post/282193/


All Articles